关于这块的开发可以参考https://flutterchina.club/developing-packages/,下面则来从0开始来构建一款自己的Flutter包发布到Flutter Package上面。
关于Flutter Package是啥应该不用过多再解释了,如果说你想在https://pub.dev/发布自己写的库供别人来使用,此时就需要学会Flutter Package的技法了,如下:
,这里贴一下网上所说有:
其中上面说到Package类型有两种,而这里首先实现的是第一种:Dart包。
咱们先来新建一个Flutter工程,可以采用命令来进行创建,如网上所示:
来吧,试一下:
bogon:workspace xiongwei$ cd flutterstudy/
bogon:flutterstudy xiongwei$ flutter create --template=package shaded_text
Creating project shaded_text...
shaded_text/LICENSE (created)
shaded_text/test/shaded_text_test.dart (created)
shaded_text/shaded_text.iml (created)
shaded_text/.gitignore (created)
shaded_text/.metadata (created)
shaded_text/pubspec.yaml (created)
shaded_text/README.md (created)
shaded_text/lib/shaded_text.dart (created)
shaded_text/.idea/libraries/Dart_SDK.xml (created)
shaded_text/.idea/modules.xml (created)
shaded_text/.idea/workspace.xml (created)
shaded_text/CHANGELOG.md (created)
Running "flutter packages get" in shaded_text... 1.2s
Wrote 12 files.
All done!
Your package code is in shaded_text/lib/shaded_text.dart
bogon:flutterstudy xiongwei$
此时在本地生成的样子瞅一下:
当然还可以利用Android Studio的向导来创建:
创建之后在Android Studio中咱们来看一下这个跟咱们正常的Flutter Application有啥不一样呢?
然后再看一下yaml配置文件:
而且还有一个区别,就是此项目貌似没法运行:
咱们这个包是要实现一个啥效果呢,其实比较简单,就是一个文本阴影的效果,如下:
其实用到的知识点在已经学习过了,是啥呢?
下面则来实现一下,定义一个Widget:
然后具体的实现相对比较简单,就不一一说明了,重点是学会如何进行发布,实现如下,先定义相关的成员变量:
library shaded_text;
import 'package:flutter/material.dart';
class ShadedText extends StatelessWidget {
final String text;
final Color textColor;
final Color shadeColor;
final double xTans;
final double yTans;
ShadedText(
{this.text, this.textColor, this.shadeColor, this.xTans, this.yTans})
: assert(text != null),
assert(textColor != null),
assert(shadeColor != null);
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
],
);
}
}
接下来则来定义Stack中的元素,这里就跟咱们之前写项目的时候不太一样了,因为这个库最终是要供别人来使用的,所以此时只需提供一个创建的行为,具体创建的细节由调用者来提供,啥意思呢?下面看一下代码体会一下:
library shaded_text;
import 'package:flutter/material.dart';
typedef ShadeBuilder = Widget Function(
BuildContext context, String text, Color);
class ShadedText extends StatelessWidget {
final String text;
final Color textColor;
final Color shadeColor;
final double xTans;
final double yTans;
final ShadeBuilder shadeBuilder;
ShadedText(
{this.text,
this.textColor,
this.shadeColor,
this.xTans,
this.yTans,
this.shadeBuilder})
: assert(text != null),
assert(textColor != null),
assert(shadeColor != null),
assert(shadeBuilder != null);
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
shadeBuilder(context, text, textColor),
Transform(
transform:
Matrix4.translationValues(xTans ?? 10.0, yTans ?? 10.0, 0.0),
child: shadeBuilder(context, text, shadeColor),
),
],
);
}
}
其中有一个小小的Dart语法复习一下,如下:
其作用是:
通常在发布的时候得给用户一个示例代码,所以此时咱们针对咱们所写的功能进行一下调用,正好可以测一下是否写得有问题,怎么创建呢?直接在Android Studio的Terminal来创建,如下:
bogon:shaded_text xiongwei$ flutter create example
Creating project example...
example/ios/Runner.xcworkspace/contents.xcworkspacedata (created)
example/ios/Runner/Info.plist (created)
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (created)
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (created)
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (created)
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (created)
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (created)
example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (created)
example/ios/Runner/Base.lproj/LaunchScreen.storyboard (created)
example/ios/Runner/Base.lproj/Main.storyboard (created)
example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (created)
example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (created)
example/ios/Flutter/Debug.xcconfig (created)
example/ios/Flutter/Release.xcconfig (created)
example/ios/Flutter/AppFrameworkInfo.plist (created)
example/test/widget_test.dart (created)
example/example.iml (created)
example/.gitignore (created)
example/.metadata (created)
example/ios/Runner/AppDelegate.h (created)
example/ios/Runner/main.m (created)
example/ios/Runner/AppDelegate.m (created)
example/ios/Runner.xcodeproj/project.pbxproj (created)
example/android/app/src/profile/AndroidManifest.xml (created)
example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (created)
example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (created)
example/android/app/src/main/res/drawable/launch_background.xml (created)
example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (created)
example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (created)
example/android/app/src/main/res/values/styles.xml (created)
example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (created)
example/android/app/src/main/AndroidManifest.xml (created)
example/android/app/src/debug/AndroidManifest.xml (created)
example/android/gradle/wrapper/gradle-wrapper.properties (created)
example/android/gradle.properties (created)
example/android/settings.gradle (created)
example/pubspec.yaml (created)
example/README.md (created)
example/lib/main.dart (created)
example/android/app/build.gradle (created)
example/android/app/src/main/java/com/example/example/MainActivity.java (created)
example/android/build.gradle (created)
example/android/example_android.iml (created)
example/.idea/runConfigurations/main_dart.xml (created)
example/.idea/libraries/Flutter_for_Android.xml (created)
example/.idea/libraries/Dart_SDK.xml (created)
example/.idea/libraries/KotlinJavaRuntime.xml (created)
example/.idea/modules.xml (created)
example/.idea/workspace.xml (created)
Running "flutter packages get" in example... 1.7s
Wrote 66 files.
All done!
[!] Flutter is partially installed; more components are available. (Channel stable, v1.2.1, on Mac OS X 10.15.6 19G73, locale zh-Hans-CN)
[!] Android toolchain - develop for Android devices is partially installed; more components are available. (Android SDK version 29.0.3)
[!] iOS toolchain - develop for iOS devices is partially installed; more components are available. (Xcode 11.2.1)
[!] Android Studio is not available. (not installed)
[!] IntelliJ IDEA Ultimate Edition is partially installed; more components are available. (version 2020.2)
[!] Connected device is not available.
Run "flutter doctor" for information about installing additional components.
In order to run your application, type:
$ cd example
$ flutter run
Your application code is in example/lib/main.dart.
bogon:shaded_text xiongwei$
此时在工程中就可以看到咱们创建的Flutter项目了,也就是之前咱们学习Flutter所用的命令,当时此时是可以运行的喽,如下:
此时咱们来调用一下咱们写的插件,如下:
接下来需要使用咱们自己写的插件,怎么用呢?其实跟我们在之前做项目使用三方库一样,需要到yaml文件中进行引用,但是咱们的库还木有往外正式发布,所以使用上略有不同,如下:
其中这个"shaded_text"名称是你插件中这块的名称:
接下来则可以来使用了:
import 'package:flutter/material.dart';
import 'package:shaded_text/shaded_text.dart';
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Shaded Text Demo'),
),
body: Center(
child: ShadedText(
text: 'Shaded Text',
textColor: Color(0xffff0000),
shadeColor: Color(0xff00ff00),
shadeBuilder: (BuildContext context, String text, Color color) =>
Container(
child: Text(
text,
style: TextStyle(color: color),
),
),
),
),
);
}
}
可以看到builder的使用方式跟我们之前在做项目时使用了大量这种方式类似,顺间领悟到了原来我们调用的内部机理,接下来运行看一下效果:
直接右键运行一下:
之后就可以看到运行的按钮可用了,效果如下:
嗯,没啥问题。
接下来则到激动人心的时刻了,准备将咱们写的高大上的DEMO给发布到Flutter的https://pub.dev/上去,能成功么?下面按照文档上的来:
此时咱们先运行dry-run来检查一下发布准备是否一切都ok:
然后在最后给出了一个错误提示了:
也就是这块配置文件中的这些信息得填一下:
还有最后一个异常需要处理:“
Your package is 146.1 MB. Hosted packages must be smaller than 100 MB. Your .gitignore has no effect since your project does not appear to be in version control.
Sorry, your package is missing some requirements and can't be published yet.
”
包太大了。。怎么办,其实上面中也有一个提示,说没有受版本的控制,那咱们将它加入到git仓库管理中试一下:
然后添加到本地仓库中:
此时再来dry-run检测一次:
居然名字后面得跟一个邮件。。好吧,改!!
然后这里还可以修改一下CHANGLOG.md文件:
另外LICENSE也可以加一下,在github中的开源项目中随便找一个:
好,此时第三次再来dry-run:
完美了,接下来尝试发布,啥命令来着:
试一下:
发现需要授权。。
Looks great! Are you ready to upload your package (y/n)? y
Pub needs your authorization to upload packages on your behalf.
In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A56482&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email
Then click "Allow access".
Waiting for your authorization...
此时点击它提示的链接进行GOOGLE账号授权一下:
接下来在Android Studio的发布命令台上就会收到授权成功的消息了,如下:
然后。。无限在处理中了。。然后最后收到这样的消息:
啥情况呢,网上搜了一下,按这博主的意思貌似是要FQ。。正好我也有梯子,那试一下呗:
这个错误没了,另一个失败又出现了,而且此时只明一个错误码出现没报具体原因。。网上继续搜,博主的意思是用-v来查看一下具体的异常:
输出日志往前看,发现用了cn中国的域名。。
这时因为咱们在Flutter学习环境搭建的时候设置了中国的镜像,为了学习顺畅嘛,身为国人都懂的,回忆一下:
所以咱们在发布的时候先将这个镜像去掉,最终官网是在国外嘛:
先注释掉,等发布成功了到时再还原,再来发布一次,注意:此时先得重启一下Android Studio再发布,不然咱们注释掉的环境变量没有生效,然后再设置一下之前的代理再发布:
那咱们改一下名称:
当然example也得改一下:
再来:
终于成功了。。咱们上官网搜一下能否搜到:
最后再将Flutter的国内镜像配置给还原。
接下来学习另一种使用场景,也就是咱们在新建项目向导中的看到的它:
看一下网上对这个插件的解释:
这里再来看一下网上的一个示意图:
其中可以看到Flutter能调用特定平台(android或ios),并且特定平台(android或ios)又调调用Flutter,达到一个互通的效果,当然目前咱们这里先只观注前者,后者在之后也会学习到,在了解了Flutter Plugin它的使用意义之后,接下来咱们则来开始实现咱们的第一个插件。
这里既可以使用Android Studio的向导来创建,也可以使用命令来创建,这里咱们用命令:
咱们试一下:
此时本地就创建了一个flutter_toast插件项目了:
此时咱们用IDE打开它:
然后咱们运行看一下默认的效果:
显示出了当前平台的版本号。
接下来咱们来简单分析一下这个默认插件的生成,搞清楚了它,基本上也就掌握了自己来写插件的思路了,从Flutter本身分析起:
先贴一下全局代码,如今学到这看这样的代码就变得很亲切了:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_toast/flutter_toast.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
platformVersion = await FlutterToast.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
),
),
);
}
}
简单撸一下:
接下来来看一下插件中是如何来获取指定平台的版本号的?
而最终方法的实现则是由具体的平台来执行的,所以咱们来看一下android平台是怎么来实现这个Flutter调用的指令的:
此时在Flutter页面就可以看到方法的结果了,再看一下ios平台的写法,基本雷同,只是语法的不同:
以上就是整个插件的使用流程。
接下来咱们依照官方的DEMO来改造第一个自己的插件,这个插件的功能就是弹个Toast出来,下面开始:
此时则在插件中增加一个showToast的方法:
接下来则在平台中来实现showToast的实现逻辑,这里以Android平台为例(ios目前还不会),如下:
package com.example.plugin.flutter_toast;
import android.content.Context;
import android.widget.TextView;
import android.widget.Toast;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
/**
* FlutterToastPlugin
*/
public class FlutterToastPlugin implements MethodCallHandler {
private Toast toast;
private Context context;
private FlutterToastPlugin(Context context) {
this.context = context;
}
/**
* Plugin registration.
*/
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_toast");
channel.setMethodCallHandler(new FlutterToastPlugin(registrar.context()));
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else if (call.method.equals("showToast")) {
String msg = call.argument("msg");
String duration = call.argument("duration");//long or short
Number textColor = call.argument("textColor");
Number textSize = call.argument("textSize");
toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
//set text
if (msg != null) {
toast.setText(msg);
}
//set duration
if (duration != null && duration.equals("long")) {
toast.setDuration(Toast.LENGTH_LONG);
} else {
toast.setDuration(Toast.LENGTH_SHORT);
}
//set styles
TextView textView = (TextView) toast.getView().findViewById(android.R.id.message);
//set text color
if (textView != null) {
textView.setTextColor(textColor.intValue());
}
//set text size
if (textView != null) {
textView.setTextSize(textSize.floatValue());
}
toast.show();
} else {
result.notImplemented();
}
}
}
此时运行看一下,发现报错了。。
这种情况咋整?其实在未来Flutter的开发中也可能会遇到这种情况,重新卸载安装一次就可以了,下面看一下:
最后插件的发布跟上面package的一样,这里就略过了。
最后还有一个咱们木有使用过,如创建向导中的这个:
简单描述就是在Android或ios原生工程中集成Flutter,这样就可以在Android原生中使用Flutter中的特性了,比较最典型的一个热重载,下面则分步骤一点点来实现这样的效果,还是只以Android平台为例,木办法,Ios的技能木有get到。。
这里新创建一个文件夹,将所有接下来要创建的工程都存放于此,便于统一管理,如下:
然后在这个目录下创建一个Android工程:
在Android工程根目录的上一级目录创建Flutter工程,保证Flutter工程与Android工程在同一级。如下:
此时的目录结构为:
此时打开咱们创建的Flutter工程,然后需要进行编译,如何搞呢?看下面:
接下来回到Android工程中添加Flutter Module的依赖:
1、修改Android项目根目录下的setting.gradle:
2、修改app下的build.gradle:
增加flutter的模块依赖:
怎么创建呢?看代码:
居然Flutter中用的不是androidx。。那怎么办?改变support方式吧:
然后还有一个地方得修改一下:
此时再导一下包就可以了:
然后布局文件也得改一下:
那现在已经在Android中内嵌了Flutter的视图了,那视图的内容怎么办呢?当然得由Flutter来提供了。
回到Flutter工程中来进行代码的编写:
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(selectWidget(window.defaultRouteName));
Widget selectWidget(String routeName) {
switch (routeName) {
case 'r1'://根据路由的名字来
return MyFlutterView();
default:
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Text(
'Unknow Route!',
style: TextStyle(color: Color(0xffff0000)),
),
),
),
);
}
}
class MyFlutterView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Card(
color: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0))),
child: Text('My Flutter View'),
),
),
),
);
}
}
其中的路由名就是咱们在Android中添加的Flutter中的路由名,如下:
好,接下来就可以来到Android工程中直接运行看下效果了:
提示ndk版本有问题,那下载一下,然后再来运行,依然报错了:
居然最小只支持8.0。。那咱们改一下最小SDK的版本:
再来运行,发现ndk库链接错误:
这个解决起来比较简单,加一个cpu的类型既可,如下:
再运行:
可以发现加载Flutter View会有一些延时,这个可能得从产品交互的角度来解决,加个loading啥的,至此Android中内嵌Flutter成功搞定。
不过目前对于Flutter的热加载功能还不支持,那就发挥不出它的优势了,所以接下来加上热加载功能,也比较简单,如下:
1、首先在Flutter Module工程目录下执行flutter attach,开始监听flutter。
此时它就在等Android工程进行启动。
2、在Android工程中运行程序,运行成功后可以在终端输入小写r热加载,大写R热重启。
重启运行Android工程,此时在Flutter的attach命令行中就会给出如下提示了:
接下来咱们就可以在Flutter Module中进行修改实时在Android项目中进行预览了,下面演示一下:
另外对于Fragment的使用也基本类似,这里就不演示了,至此对于Flutter的高级技法就已经学习完毕啦,这些待实际项目中再来进行实践,接下来打算开启一个全新的Flutter的项目操练,要想彻底掌握,别无它法,只能勤加练习~~