Flutter —— packages 和 plugin
1. Packages
1.1 简介
Dart package 最低要求是包含一个 pubspec.yaml 文件,package只包含dart部分。此外,一个 package 可以包含依赖关系 (在 pubspec.yaml 文件里声明)、 Dart 库、应用、资源、测试、图片和例子等,开发者可以使用package 快速构建应用。
1.2 创建 Packages
这里新建一个Flutter Package 项目,可以看到这里面是没有Android和iOS的文件的。
将之前的 friends页面里面的index_bar的内容拷贝到test_package_demo里面,去除掉没有的import,然后将所需要的内容拷贝进来。
library test_package_demo;
import 'package:flutter/material.dart';
class IndexBar extends StatefulWidget {
final void Function(String str) indexBarCallBack;
const IndexBar({Key? key, required this.indexBarCallBack}) : super(key: key);
@override
_IndexBarState createState() => _IndexBarState();
}
String getIndex(BuildContext context, Offset globalPosition) {
//拿到当前小部件
RenderBox box = context.findRenderObject() as RenderBox;
//拿到y值,globalToLocal当前位置距离部件的原点的距离
double y = box.globalToLocal(globalPosition).dy;
// 算出字符高度
var itemHeight = screenHeight(context) / 2 / INDEX_WORDS.length;
// 算出第几个item
int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
return INDEX_WORDS[index];
}
class _IndexBarState extends State<IndexBar> {
Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
Color _textColor = Colors.black;
@override
void initState() {
// TODO: implement initState
}
@override
Widget build(BuildContext context) {
final List<Widget> words = [];
for (int i = 0; i < INDEX_WORDS.length; i++) {
words.add(Expanded(
child: Text(
INDEX_WORDS[i],
style: TextStyle(fontSize: 10, color: _textColor),
),
));
}
return Positioned(
right: 0.0,
top: screenHeight(context) / 8,
height: screenHeight(context) / 2,
width: 30,
child: GestureDetector(
onVerticalDragDown: (DragDownDetails details) {
setState(() {
_bkColor = const Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
});
},
onVerticalDragUpdate: (DragUpdateDetails details) {
getIndex(context, details.globalPosition);
},
onVerticalDragEnd: (DragEndDetails details) {
setState(() {
_bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
_textColor = Colors.black;
});
},
child: Container(
child: Column(
children: words,
),
color: _bkColor,
)),
);
}
}
//主题色
const Color weChatThemColor = Color.fromRGBO(220, 220, 220, 1.0);
//屏幕宽高
double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double screenHeight(BuildContext context) => MediaQuery.of(context).size.height;
const INDEX_WORDS = [
'🔍',
'☆',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
];
这里面使用到了资源,那么就需要进行处理。在package 中添加一个Directory images,然后添加图片进去。
然后到pubspec.yaml 里面添加images的引用。
然后看到顶部这里的四个参数。
- name:包的名称
- description: 包的描述
- version: 包的版本
- homepage:包的主页,需要是可以访问的否则会扣分
1.3 发布 Package
接下来要发布刚才创建的Package,那么先使用终端来到Package的文件夹中。
首先需要使用命令检查包是否有问题.
flutter packages pub publish --dry-run
这里看到刚才创建的包是没有问题的 如果把刚才四个参数中的某一个去掉那么就会报错。
如果检查通过了之后,那么就可以使用命令来发布Package了。
flutter packages pub publish
发布的时候需要注意
- 发布的时候需要科学上网
- 需要验证谷歌账号,所以需要有一个谷歌账号。
这里来到pub.dev 然后登陆谷歌账号。 发布之后会会提醒我们去一个节目然后点击Allow access 进去之后登陆就会提示Pub Authorized Successfully。 这里出来之后发现还是有License 的问题,这个证书需要在github创建,是版权相关的问题。
在github新建项目,点击Choose a license然后选择BSD 3 接着就可以使用这里的License 文件了。 那么这个时候就可以发布了。 这个时候就可以在pub.dev搜索到上传的package 了。 也可以在My packages 里面看到。
1.4 Package在项目中应用
来到之前的项目中来使用这个package。 在pubspec.yaml 里面的dependencies添加刚才的package。 可以在External libraries的 dark packages 里面看到Package被成功下载下来了。 接着来到friends_page中使用,为了不冲突使用as来取别名。
import 'package:test_package_demo/test_package_demo.dart' as ls;
这时候就可以使用包中的IndexBar了。
这里其实还有两个问题
这里如果要把图片也加入到包里面,那么就将包里面的images拖到lib里面。
然后在使用图片的时候指定包名,这样就可以解决图片问题了。 改完了之后,在pubspec.yaml里面修改一下version,重新发布一下Package,然后到项目中修改一下包版本。 还需要在assets里面添加图片的引用,这里需要是完整的路径。
- packages/test_package_demo/images/bubble.png
这样上面的问题就解决了,但是可以看到引用图片的话就非常的麻烦,所以需要对包进行修改。在包里面的IndexBar给一个参数让外界可以传图片进来。
final void Function(String str) indexBarCallBack;
final ImageProvider? image;
final Icon? icon;
const IndexBar(
{Key? key, required this.indexBarCallBack, this.image, this.icon})
: super(key: key);
然后在图片那根据传进来的参数进行判断显示。
widget.icon ??
Image(
image: widget.image ??
const AssetImage('images/bubble.png',
package: "test_package_demo"),
width: 60,
),
1.5 Package分数
Package的分数对于一个包是很重要的,如果分数过低,那么基本就不会有人用这个包。对于新创建的包来说,一般只需要注意2个点
- description的长度 (60 - 180个字)
- 提供example
描述都会写,这里来创建一个示例工程。 新建一个项目,然后到pubspec.yaml里面添加包。 在工程里面代码
import 'package:flutter/material.dart';
import 'package:test_package_demo/test_package_demo.dart' as ls;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: FriendsPage(),
);
}
}
class _FriendCell extends StatelessWidget {
const _FriendCell({this.imageUrl, this.name, this.groupTitle, this.imageAssets});
final String? imageUrl;
final String? name;
final String? groupTitle;
final String? imageAssets;
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 10),
height: groupTitle != null ? 30 : 0,
color: ls.weChatThemColor,
child: groupTitle != null
? Text(
groupTitle!,
style: TextStyle(color: Colors.grey),
)
: null,
), //头部
Container(
color: Colors.white,
child: Row(
children: [
Container(
margin: EdgeInsets.all(10),
width: 34,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(
image: imageUrl != null
? NetworkImage(imageUrl!)
: AssetImage(imageAssets!,package: "test_package_demo") as ImageProvider,
)),
), //图片
Container(
// color: Colors.red,
width: ls.screenWidth(context) - 54,
child: Column(
children: [
Container(
alignment: Alignment.centerLeft,
height: 54,
child: Text(
name!,
style: TextStyle(fontSize: 18),
),
),
Container(
height: 0.5,
color: ls.weChatThemColor,
), //下划线
],
),
), //昵称+下划线
],
),
), //Cell的内容
],
);
}
}
class FriendsPage extends StatefulWidget {
@override
_FriendsPageState createState() => _FriendsPageState();
}
class _FriendsPageState extends State<FriendsPage>
with AutomaticKeepAliveClientMixin<FriendsPage> {
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
double _cellHeight = 54.5;
double _groupHeight = 30.0;
//字典,里面放item和高度的对应的数据。
final Map _groupOffsetMap = {
ls.INDEX_WORDS[0]: 0.0,
ls.INDEX_WORDS[1]: 0.0,
};
final List<Friends> _headerData = [
Friends(imageAssets: 'images/新的朋友.png', name: '新的朋友'),
Friends(imageAssets: 'images/群聊.png', name: '群聊'),
Friends(imageAssets: 'images/标签.png', name: '标签'),
Friends(imageAssets: 'images/公众号.png', name: '公众号'),
];
final List<Friends> _listDatas = [];
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
//创建数据
_listDatas
..addAll(datas)
..addAll(datas);
//排序
_listDatas.sort((Friends a, Friends b) {
return a.indexLetter!.compareTo(b.indexLetter!);
});
var _groupOffset = _cellHeight * _headerData.length;
//进过循环计算,将每一个头的位置算出来。放入字典
for (int i = 0; i < _listDatas.length; i++) {
if (i < 1) {
//第一个cell一定有头!
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
//保存完了再加_groupOffset
_groupOffset += _cellHeight + _groupHeight;
} else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
//不同存,只需要加Cell的高度
_groupOffset += _cellHeight;
} else {
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
//保存完了再加_groupOffset
_groupOffset += _cellHeight + _groupHeight;
}
}
}
Widget _itemForRow(BuildContext context, int index) {
//显示头部4个Cell
if (index < _headerData.length) {
return _FriendCell(
imageAssets: _headerData[index].imageAssets,
name: _headerData[index].name,
);
}
//是否显示组名字!
bool _hiddenIndexLetter = (index - 4 > 0 &&
_listDatas[index - 4].indexLetter == _listDatas[index - 5].indexLetter);
return _FriendCell(
imageUrl: _listDatas[index - 4].imageUrl,
name: _listDatas[index - 4].name,
groupTitle: _hiddenIndexLetter ? null : _listDatas[index - 4].indexLetter,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
appBar: AppBar(
// actions: [
// GestureDetector(
// onTap: () {
// // Navigator.of(context).push(MaterialPageRoute(
// // builder: (BuildContext context) => DiscoverChildPage(
// // title: '添加朋友',
// // )));
// },
// child: Container(
// margin: EdgeInsets.only(right: 10),
// child: Image(
// image: AssetImage('images/icon_friends_add.png'),
// width: 25,
// ),
// ),
// ),
// ],
backgroundColor: ls.weChatThemColor,
title: Text('通讯录'),
),
body: Stack(
children: [
Container(
color: ls.weChatThemColor,
child: ListView.builder(
controller: _scrollController,
itemBuilder: _itemForRow,
itemCount: _listDatas.length + _headerData.length,
),
), //列表
ls.IndexBar(
indexBarCallBack: (String str) {
if (_groupOffsetMap[str] != null) {
_scrollController.animateTo(_groupOffsetMap[str],
duration: Duration(microseconds: 100),
curve: Curves.easeIn);
}
},
), //悬浮的索引条
],
));
}
}
class Friends {
Friends({this.imageUrl, this.name, this.indexLetter, this.imageAssets});
final String? imageAssets;
final String? imageUrl;
final String? name;
final String? indexLetter;
}
List<Friends> datas = [
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/27.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/17.jpg',
name: '菲儿',
indexLetter: 'F'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/16.jpg',
name: '安莉',
indexLetter: 'A'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/31.jpg',
name: '阿贵',
indexLetter: 'A'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/22.jpg',
name: '贝拉',
indexLetter: 'B'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/37.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/18.jpg',
name: 'Nancy',
indexLetter: 'N'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/47.jpg',
name: '扣扣',
indexLetter: 'K'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/3.jpg',
name: 'Jack',
indexLetter: 'J'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/5.jpg',
name: 'Emma',
indexLetter: 'E'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/24.jpg',
name: 'Abby',
indexLetter: 'A'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/15.jpg',
name: 'Betty',
indexLetter: 'B'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/13.jpg',
name: 'Tony',
indexLetter: 'T'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/26.jpg',
name: 'Jerry',
indexLetter: 'J'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/36.jpg',
name: 'Colin',
indexLetter: 'C'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/12.jpg',
name: 'Haha',
indexLetter: 'H'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/11.jpg',
name: 'Ketty',
indexLetter: 'K'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/13.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/23.jpg',
name: 'Lina',
indexLetter: 'L'),
];
然后到pubspec.yaml添加用到的图片。
然后回到package里面添加directory 并且将刚才新建的example工程的main.dart文件扔进去。
这样example就添加好了。
1.6 Part
如果一个包代码都在一个文件中,就会太拥挤,这个时候就可以用到part。这里在包里面的lib中创建一个index_bar文件,然后将在test_package_demo中写的内容都放到index_bar里面。然后添加下列代码告诉外界index_bar属于test_package_demo。
part of 'test_package_demo.dart';
然后在test_package_demo里面添加
part "index_bar.dart";
到这里package修改就完成了,将版本号修改一下然后重新上传
然后就可以看到Example和新的版本号了。
2. Plugin
如果要开发一个调用特定平台API的包,那么就需要开发一个插件包,插件包是Dart包的专用版本。
创建一个Plugin工程。可以看到这里默认有一个获取版本的方法。
在这里添加一个获取电量的方法。
static Future<String?> get platformBatteryLevel async {
int batteryLevel = await _channel.invokeMethod('platformBatteryLevel');
return batteryLevel.toString();
}
然后来到example中的iOS打开Runner.xcworkspace编写代码。将插件中iOS的FlutterPluginDemoPlugin的代码复制到Runner.xcworkspace的Appdelegate里面开始编写,因为这里面有提示编写比较方便。
然后后的代码:
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"platformBatteryLevel" isEqualToString:call.method]) {
int batteryLevel = [self getBatteryLevel];
result(@(batteryLevel));
}else {
result(FlutterMethodNotImplemented);
}
}
- (int) getBatteryLevel {
UIDevice *device = UIDevice.currentDevice;
device.batteryMonitoringEnabled = YES;
if (device.batteryLevel == UIDeviceBatteryStateUnknown) {
return -1;
} else {
return (int)(device.batteryLevel * 100);
}
}
然后将写好的代码放到FlutterPluginDemoPlugin.m里面。
接下来到example里面的main.dart进行试验。
运行后得到下图,这里因为是模拟器所以是-100.
|