六虎
(更多…)
跟着 SPM( Swift Package Manager ) 功能的不断完善,越来越多的开发者开始在他的项目中经过创立多个 Package 的办法来别离功能、办理代码。SPM 自身供给了对包中各类资源( 包括本地化资源 )的办理才能,但首要局限于在本包中运用这些资源,难以将资源进行同享。在有多个 Target 均需调用同一资源的状况下,原有的办法很难应对。本文将介绍一种在具有多个 SPM 包的项目中,对资源进行一致办理的办法。
老司机 iOS 周报,只为你呈现有价值的信息。
项目运用 Flutter 2.10.2 版别,您可运用 >=2.x 的版别:
Flutter 2.10.2
Flutter 2.10.2 • channel stable • https://github.com/flutter/flutter.git Framework • revision 097d3313d8 Engine • revision a83ed0e5e3 Tools • Dart 2.16.1 • DevTools 2.9.2
挑选要装置 Flutter 的操作体系,并依照官方操作流程进行 Flutter 环境的装置及验证:
在 VS Code 中开发(个人主张运用轻量的 vscode 开发)
项目创立:
在您的作业目录中运用命令行东西:
flutter create flutter_demo
flutter_demo 是项目称号(替换为你的项目称号),官方主张文件称号以下划线命名办法。
flutter_demo
项目目录结构:
├─assets // 静态文件 ├─lib // 首要作业目录,编写大部分代码放在这儿 │ ├─apis // api 文件 │ ├─components // 公共组件 │ ├─models // 数据模型 │ ├─router // 项目路由 │ ├─utils // 东西类办法 │ ├─views // 商城页面UI │ ├─widgets // 事务相关组件 │ ├─app_theme.dart // 自定义主题文件 │ └─mian.js // 项目进口文件 ├─android // android 编译目录 ├─ios // ios 编译目录 ├─pubspec.yaml // 项目装备文件
运转项目:
准备虚拟机
$ open -a Simulator
在终端履行指令 flutter run 运转项目
flutter run
创立自定义主题类,设定项目UI色彩,自定义各类文字款式,便利一致项目UI,灵敏布局。
编写主题类:
创立文件 lib/app_theme.dart, 创立 AppTheme类并规划主题色彩和文字款式
lib/app_theme.dart
AppTheme
import 'package:flutter/material.dart'; class AppTheme { AppTheme._(); // 色彩设置 static const Color notWhite = Color(0xFFECF0EF); static const Color nearlyWhite = Color(0xFFFFFFFF); // 文字款式 static const TextTheme textTheme = TextTheme( headline4: display1, headline5: headline, headline6: title, subtitle2: subtitle, bodyText1: body1, bodyText2: body2, // bodyText2: flutter默许文字款式 caption: caption, ); static const TextStyle display1 = TextStyle( fontFamily: fontName, fontWeight: FontWeight.bold, fontSize: 36, letterSpacing: 0.5, height: 0.9, color: darkerText, ); ... }
引进并运用主题: main.dart 进口文件中引进主题类并运用
main.dart
import 'package:shopping_mall/app_theme.dart'; @override Widget build(BuildContext context) { return MaterialApp( title: '青山商城', debugShowCheckedModeBanner: false, // 主题装备 theme: ThemeData( primarySwatch: Colors.teal, fontFamily: AppTheme.fontName, // 默许字体 textTheme: AppTheme.textTheme, // 文字主题 ), onGenerateRoute: onGenerateRoute, initialRoute: '/', home: const AppHomeScreen(), ); }
设置状况栏款式: main.dart 进口文件中运用 setSystemUIOverlayStyle
setSystemUIOverlayStyle
void main() async { WidgetsFlutterBinding.ensureInitialized(); // 设置状况栏 SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( // 顶部状况栏色彩 statusBarColor: Colors.transparent, // 顶部状况栏图标的亮度 statusBarIconBrightness: Brightness.dark, // 顶部状况栏的亮度 statusBarBrightness: !kIsWeb && Platform.isAndroid ? Brightness.dark : Brightness.light, // 体系底部导航栏的色彩 systemNavigationBarColor: Colors.white, // 体系底部导航栏分割线色彩 systemNavigationBarDividerColor: Colors.transparent, // 体系导航栏图标的亮度 systemNavigationBarIconBrightness: Brightness.dark, )); // 设置设备方向 await SystemChrome.setPreferredOrientations(<DeviceOrientation>[ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown ]).then((_) => runApp(const ProviderScope(child: MyApp()))); }
新建 lib/routers/routes.dart 文件:
lib/routers/routes.dart
import 'package:flutter/material.dart'; import 'package:shopping_mall/views/detail/detail_index.dart'; import 'package:shopping_mall/views/goods/goods_list_index.dart'; /// 路由生成器 Route onGenerateRoute(RouteSettings settings) { switch (settings.name) { case '/detail': return MaterialPageRoute( builder: (_) => const DetailIndex(), settings: settings); case '/goods': return MaterialPageRoute( builder: (_) => const GoodsListIndex(), settings: settings); default: } }
mian.dart 中运用路由生成器特点 onGenerateRoute:
mian.dart
onGenerateRoute
return MaterialApp( title: '青山商城', // 路由生成器 onGenerateRoute: onGenerateRoute, // 初始路由途径 initialRoute: '/', home: const AppHomeScreen(), );
路由跳转与传参:
// 跳转到详情页,传参产品id Navigator.of(context).pushNamed('/detail', arguments: {'id': goods.id}); // 跳转到主页,传参产品tab,并移除前史路由 Navigator.of(context).pushNamedAndRemoveUntil( '/', (route) => false, arguments: {'tab': 3}); // 页面获取传过来的参数 Map args = ModalRoute.of(context)?.settings.arguments as Map; int id = args['id'];
运用 dio 库,dio 封装查看: flutter_dio_util
dio
// 简略运用 import 'package:dio/dio.dart'; void main() async { var dio = Dio(); final response = await dio.get('https://google.com'); print(response.data); } // response.data 这儿回来的是json字符串,可运用 data['name'] 获取值 // 要想像 Object 相同运用 data.name 获取。需求转化为Dart模型(Json to Dart)
假如有如下 Person Json数据
Person
{ "name": "jsdawn", "age": 18, "gender": "" }
运用 quicktype 将 Json 快速转化 为 Dart 模型 person.dart
person.dart
// To parse this JSON data, do // final person = personFromJson(jsonString); import 'dart:convert'; // 字符串转为`Person`模型 Person personFromJson(String str) => Person.fromJson(json.decode(str)); // `Person`数据转为字符串 String personToJson(Person data) => json.encode(data.toJson()); class Person { Person({ this.name, this.age, this.gender, }); String name; int age; String gender; // json中字段不可少,不然需求判空:json["name"] ?? "" factory Person.fromJson(Map<String, dynamic> json) => Person( name: json["name"], age: json["age"], gender: json["gender"], ); Map<String, dynamic> toJson() => { "name": name, "age": age, "gender": gender, }; }
在 flutter 中运用:
// 这儿 DefaultAssetBundle 的 loadString 加载 Json 文件,模拟接口调用 String jsonString = await DefaultAssetBundle.of(context) .loadString('assets/json/data.json'); // 字符串转为 Dart Model final person = personFromJson(jsonString); print(person.name); // jsdawn
运用精巧的svg图标,多一份酷爱,多一份色彩。
引进图标资源:
从iconfont.cn下载图标的svg文件,放入assets/svg目录。在 pubspec.yaml 中装备静态资源:
assets/svg
pubspec.yaml
assets: - assets/svg/
引进flutter_svg插件并运用:
flutter_svg
在项目终端履行指令 flutter pub add flutter_svg 下载插件。在页面中运用,以主页产品类型为例
flutter pub add flutter_svg
import 'package:flutter_svg/svg.dart'; Padding( padding: const EdgeInsets.symmetric(vertical: 4), // SvgPicture.asset 加载svg静态资源 child: SvgPicture.asset('assets/svg/category_phone.svg', height: 35), ),
产品列表项的高度或许不同,比方标题有一行也有两行。运用瀑布流布局,可完美和谐各块的高度。
怎么挑选插件:
在flutter插件商场 pub.dev 中搜索瀑布流布局插件,你找到的或许会许多,怎么挑选最适合当时项目的呢?
比方咱们项目开发环境是 flutter v2.x 版别,该版别最大的特点是支撑 Null Safety。所以首要需求支撑 Null Safety,然后挑选 LIKES 数量多而且契合瀑布流需求的插件。
flutter v2.x
Null Safety
LIKES
引进插件并运用:
在项目终端履行指令 flutter pub add flutter_staggered_grid_view 下载插件。在项目中运用瀑布流布局编写产品列表组件
flutter pub add flutter_staggered_grid_view
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; @override Widget build(BuildContext context) { // 运用插件的 MasonryGridView 类编写瀑布流布局 return MasonryGridView.count( controller: _scrollController, padding: widget.list.isNotEmpty ? const EdgeInsets.symmetric(vertical: 12) : null, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 2, // 列的数量 mainAxisSpacing: 20, // 主轴项之间的距离 crossAxisSpacing: 12, // 穿插轴项之间的距离 itemCount: widget.list.length, // 列表项生成器 itemBuilder: (context, index) { return GoodsListItem(widget.list[index]); }, ); }
购物车算是商城的中心功用之一,也有较杂乱的逻辑。
购物车列表项UI如图所示,产品特点结合类型拟定Json数据:
{ "id": 46, "title": "能方研影", "price": 61.96, "cover": "http://dummyimage.com/300x300/B9C6C3&text=shopping%20mall", "count": 3, "color": "蓝色", "size": "XS", }
Json to Dart:前往quicktype,将Json数据复制进去转化为 Dart 数据模型(当然也能够手动编写)
Dart 数据模型
class CartInfoModel { CartInfoModel({ required this.id, required this.title, required this.price, required this.cover, required this.count, required this.color, required this.size, }); int id; String title; double price; String cover; int count; String color; String size; ... }
项目运用 Flutter 官方引荐的 riverpod (一种反应式缓存和数据绑定结构) 进行购物车状况办理,让数据能够跨组件运用。
在项目终端履行指令flutter pub add flutter_riverpod下载插件。创立 lib/providers/cart_provider.dart文件:
flutter pub add flutter_riverpod
lib/providers/cart_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart'; ... const cartPrefsKey = 'cartList'; class CartNotifier extends ChangeNotifier { // 购物车列表 final List<CartInfoModel> cartList = <CartInfoModel>[]; // 总数量 int get totalCount { int total = 0; for (var item in cartList) { total += item.count; } return total; } // 总价格 double get totalPrice { double total = 0; for (var item in cartList) { total += item.price * item.count; } return total; } // 参加购物车 // 更新购物车数量 // 移除购物车指定项 // 查询/初始化购物车 } // ChangeNotifierProvider 用法 final cartProvider = ChangeNotifierProvider<CartNotifier>((ref) { return CartNotifier(); });
注册大局状况:
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { runApp( // ProviderScope包裹进口组件,供给大局状况 ProviderScope(child: MyApp()) ); }
在组件中读取:
// 读取购物车列表 List<CartInfoModel> list = ref.watch(cartProvider.select((value) => value.cartList)); // 读取结算价格 double totalPrice = ref.watch(cartProvider.select((value) => value.totalPrice)); // 读取总数量 int totalCount = ref.watch(cartProvider.select((value) => value.totalCount)); // 调用办法 ref.read(cartProvider.notifier).pushCart(_cartInfo);
首要查找同一标准的产品,有则更新数量,无则参加购物车。参加购物车的 cartInfo 需求深复制防止影响再次挑选该产品其他标准的时分改动购物车数据。购物车数据改变后需求调用 notifyListeners 告诉状况办理器更新状况。
cartInfo
notifyListeners
/// 参加购物车 void pushCart(CartInfoModel cartInfo) { // 查找 id/color/size 都相同的购物车产品 int idx = cartList.indexWhere((item) => (item.id == cartInfo.id && item.color == cartInfo.color && item.size == cartInfo.size)); if (idx > -1) { // 存在则数量+1 cartList[idx].count += cartInfo.count; } else { // 不存在则增加(这儿深复制cartInfo) cartList.add(cartInfoModelFromJson(cartInfoModelToJson(cartInfo))); } // 告诉 CartNotifier 状况更新 notifyListeners(); // 更新数据存储到本地 updCartPrefs(); } /// 更新购物车数量 void updCartCount(CartInfoModel cartInfo, int count) { // 查找 id/color/size 都相同的购物车产品 int idx = cartList.indexWhere((item) => (item.id == cartInfo.id && item.color == cartInfo.color && item.size == cartInfo.size)); if (idx > -1) { // 存在则更新数量 cartList[idx].count = count; } notifyListeners(); // 更新数据存储到本地 updCartPrefs(); } /// 移除购物车指定产品 void renmoveCart(CartInfoModel cartInfo) { // 查找 id/color/size 都相同的购物车产品 int idx = cartList.indexWhere((item) => (item.id == cartInfo.id && item.color == cartInfo.color && item.size == cartInfo.size)); if (idx > -1) { // 存在则移除该项 cartList.removeAt(idx); } notifyListeners(); // 更新数据存储到本地 updCartPrefs(); }
需求: 产品参加到购物车,还未结算并退出运用,下次进来购物车产品还在。 思路: 每次购物车数据有改动需求把数据更新到本地存储,下次翻开App的时分从本地康复数据到购物车。 耐久化存储: 项目运用 shared_preferences 插件进行本地耐久化存储。以key-value的形式存储字符串到本地。
shared_preferences
key-value
在项目终端履行指令 flutter pub add shared_preferences 下载插件,在 cart_provider.dart 购物车状况办理类中运用:
flutter pub add shared_preferences
cart_provider.dart
import 'package:shared_preferences/shared_preferences.dart'; class CartNotifier extends ChangeNotifier { final List<CartInfoModel> cartList = <CartInfoModel>[]; ... /// 更新购物车数据到本地耐久化存储 void updCartPrefs() async { final prefs = await SharedPreferences.getInstance(); // 购物车数据转为字符串 String cartString = json.encode(cartList).toString(); // `setString`存储到本地 prefs.setString('cartList', cartString); } /// 查询/初始化 - 从本地读取数据康复到购物车状况 Future<List<CartInfoModel>> getCartList() async { // 获取耐久化实例 final prefs = await SharedPreferences.getInstance(); // 调用`getString`办法读取本地字符串数据 String? _cartString = prefs.getString('cartList'); cartList.clear(); // 字符串数据转化为`CartInfoModel`初始化到购物车列表 if (_cartString != null) { List<CartInfoModel> _list = cartInfoModelListFromJson(_cartString); cartList.addAll(_list); } notifyListeners(); return cartList; } }
初始化购物车:
用户每次翻开运用,康复购物车数据:在进口文件 main.dart 中调用 getCartList 办法初始化购物车数据。
getCartList
import 'package:flutter_riverpod/flutter_riverpod.dart'; // flutter_riverpod 中的 `ConsumerStatefulWidget` 类 // 在状况组件中供给 ref 以运用状况办理器 class MyApp extends ConsumerStatefulWidget { const MyApp({Key? key}) : super(key: key); @override ConsumerState<MyApp> createState() => _MyAppState(); } class _MyAppState extends ConsumerState<MyApp> { @override void initState() { super.initState(); // 调用`getCartList`办法初始化购物车 ref.read(cartProvider).getCartList(); } @override Widget build(BuildContext context) { return MaterialApp( title: '青山商城', ); } }
代码写的好不如产品包装的好,打包也是至关重要的一步。由于设备有限,项目只亲测了安卓apk打包,其他终端的打包办法参阅 官方打包布置
项目终端履行指令,等候打包结束后提示apk方位。--split-per-abi 按abi拆分apk,减小包体积
--split-per-abi
flutter build apk --split-per-abi
Android:在 android/app/src/main/AndroidManifest.xml 中修正android:label="运用称号"
android/app/src/main/AndroidManifest.xml
android:label="运用称号"
iOS:在 ios/Runner/Info.plist 中修正 CFBundleName 对应的Value
ios/Runner/Info.plist
CFBundleName
Android: 在 android/app/src/res/mipmap 文件夹中替换相应图片
android/app/src/res/mipmap
iOS:在 ios/Runner/Assets.xcassets/AppIcon.appiconset 文件夹中替换相应尺度的图片。假如运用不同的文件名,还需更新同一目录中的 Contents.json 文件中的 filename
ios/Runner/Assets.xcassets/AppIcon.appiconset
Contents.json
filename
Android:在 android/app/src/res/drawable/launch_background.xml 文件完成自定义发动界面,这儿的是xml语法。也能够运用默许模版,在 android/app/src/res/mipmap 相应尺度文件下放入发动图片launch_image:
android/app/src/res/drawable/launch_background.xml
launch_image
<item> <bitmap android:gravity="center" android:src="@mipmap/launch_image" /> </item>
iOS:在 ios/Runner/Assets.xcassets/LaunchImage.imageset 文件夹中替换相应尺度的图片。假如运用不同的文件名,还需更新同一目录中的 Contents.json 文件中的 filename
ios/Runner/Assets.xcassets/LaunchImage.imageset
若需求将 apk 发布到各大运用商场,比方运用宝等,需求对app进行签名。
key.jks
android/app/
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
android/key.properties
storePassword=<上一过程中的暗码> keyPassword=<上一过程中的暗码> keyAlias=key storeFile=key.jks
android/app/build.gradle
android
// 新增的内容 def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } // android 代码块 android { ... }
修正 signingConfigs 和 buildTypes 代码块:
signingConfigs
buildTypes
// 新增内容 signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } // 替换的内容 buildTypes { release { signingConfig signingConfigs.release } }
apk打包后网络图片不显现:
在 android/app/src/profile/AndroidManifest.xml 中答应网络权限(若无效则需求在 android/app/src/main/AndroidManifest.xml也装备一份):
android/app/src/profile/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
空安全过错(flutter v2):
假如你运用的是 flutter v2 版别或许更高版别,运转或打包时报空安全过错(null safety)。是由于你在项目中的部分写法或许插件不支撑空安全标准,主张替换支撑 Null Safety 的插件。或许运用以下指令 --no-sound-null-safety 跳过 Null Safety 检测。
flutter v2
--no-sound-null-safety
flutter build apk --no-sound-null-safety
能够看到,文章最初的 Flutter商城APP实战知识点图,项目知识点大大小小有几十个,由于精力有限,抽取了几个首要知识点解说。假如咱们想要一同学习图中 其他的知识点,能够在谈论区 留言, 每周会抽取时刻 更新文章。
Flutter商城APP实战知识点图
博观而约取,厚积而薄发 记得 点赞 + ❤️保藏,➕重视不走失
项目Gitee:gitee.com/jsdawn/shop…
转载声明: 请注明作者,注明原文链接,有疑问致邮 kingwyh1993@163.com
flutter 从零开发一个电子商城APP,一套代码库能够高效构建多平台精巧运用(包含安卓App/苹果IOSApp/苹果mac运用/windows运用) (更多…)
flutter 从零开发一个电子商城APP,一套代码库能够高效构建多平台精巧运用(包含安卓App/苹果IOSApp/苹果mac运用/windows运用)
flutter
2018年的时候需要换电脑,因为自己没有打游戏的需求,电脑主要也就是用来做开发的,或者看看视频,苹果电脑能满足自己的需求,就选择了MacBook Pro。
Firebase帮助你开发、衡量、改进和发展你的移动应用。它得到了谷歌的支持,涵盖了广泛的服务,包括实时数据库、认证、崩溃监测、分析、推送通知等。Firebase将所有这些后端、平台相关的工具作为一项服务来提供,因此你可以更专注于构建应用的核心功能。
本文已参与「新人创作礼」活动,一起开启创作之路。
日常开发中对GCD用的最多的便是async和sync,也便是异步去做和同步去做某个使命,但是关于GCD来说可不止单单这个功用,本文首要看下GCD的其他功用如栅门函数、信号量、调度组以及dispatch_once等等。
GCD
async和sync
异步去做和同步去做
栅门函数、信号量、调度组以及dispatch_once
最近,苹果又来“整顿”手机厂商了。