百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

5分钟搞定Flutter与Android 的交互(内附大量Flutter学习资源)

bigegpt 2024-08-07 17:50 9 浏览

码个蛋(codeegg)第 743 次推文

Flutter 说到底只是一个 UI 框架,很多功能都需要通过原生的 Api 来实现,那么就会涉及到 Flutter 和 Native 的交互,因为本人不懂 iOS 开发,所以只能讲下 Flutter 同 Android 的交互。

Android项目配置Flutter依赖

既然是互相交互,那么需要准备一个 Android 项目。接着就需要创建 flutter module,让 Android 项目依赖,创建的方法可以参考官网 Flutter Wiki,虽然是官网提供的方法,但是完全按照这个步骤来,还是会有坑的,这边就慢慢一步步解决坑。

如果你用的是 Android Studio 进行开发的话,直接打开底部的 Terminal,直接创建 flutter module 依赖

flutter create -t module flutter_native_contact至于 module 名可以随意填写,module 创建完后结构大概是这样的

flutter module.png

接着切换到 module 下的 .android 文件夹,接着有坑来了,官网提供的方法是 ./gradlew flutter:assembleDebug可能会提示命令不存在,那么直接通过gradlew flutter:assembleDebug来运行,等它自动跑完后,打开根目录下的settings.gradle文件,加入官网提供的 gradle 代码

setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_native_contact/.android/include_flutter.groovy' // new
))
// new

你以为这里没坑,真是图样图森破,没坑是不可能的,编译器大爷可能会给你甩这么个错误

很明显可以看出是找不到我们的文件,所以把文件名路径给补全

evaluate(new File( // new
settingsDir.parentFile, // new
'FlutterNativeContactDemo/flutter_native_contact/
.android/include_flutter.groovy' // 这里补全路径
))

接着打开原有项目下,原有项目下,原有项目下的 app 中的 build.gradle文件,在 android 下加上如下代码

compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}

这个必须要加,不要问为什么,我也不知道为什么,最后在项目下添加 flutter module 的依赖就完成了。这个过程告诉我们一个什么道理呢?*不要以为官网的都对,官网讲的也不是完全可信的,时不时给你来个坑就能卡你老半天。

原生界面加载Flutter页面

那么如何在原生界面显示 Flutter 界面呢,这个就需要通过 FlutterView 来实现了,Flutter 这个类提供了 createViewcreateFragment两个方法,分别用于返回 FlutterView 和 FlutterFragment 实例,FlutterFragment 的实现原理也是通过 FlutterView 来实现的,可以简单看下 FlutterFragment 的源码

/**
* A {@link Fragment} managing a {@link FlutterView}.
*
* <p><strong>Warning:</strong> This file is auto-generated by Flutter tooling.
* DO NOT EDIT.</p>
*/
public class FlutterFragment extends Fragment {
public static final String ARG_ROUTE = "route";
private String mRoute = "/";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取传入的路由值,默认为 '/'
if (getArguments != ) {
mRoute = getArguments.getString(ARG_ROUTE);
}
}

@Override
public FlutterView onCreateView(@Non LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
// 最后还是挺过 createView 方法来生成页面,只不过直接放在 fragment,
// 放在 fragment 会比直接 使用 FlutterView 更方便管理,例如实现 ViewPager 等
return Flutter.createView(getActivity, getLifecycle, mRoute);
}
}

CreateFragment方式加载

在原生页面显示 Flutter 界面的第一种方式就是加载 FlutterFragment,看个比较简单的例子吧

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<!-- 这个布局用于加载 fragment -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/flutter_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginBottom="50dp"
android:src="@drawable/ic_add_white_36dp"
app:fabSize="auto"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

</android.support.constraint.ConstraintLayout>

在 Activity 可以直接通过返回 FlutterFragment 加载到 FrameLayout 即可

class MainActivity : AppCompatActivity {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

supportFragmentManager.beginTransaction
.add(R.id.fragment_container,
Flutter.createFragment("route_flutter"))
.commit
}
}

这样就把 Flutter 页面加载到原生界面了,会通过传递的路由值在 dart 层进行查找,所以接着就需要编写 Flutter 界面

/// runApp 内部值也可以直接传入 _buildWidgetForNativeRoute 方法
/// 这边在外层嵌套一层 MaterialApp 主要是防止一些不必要的麻烦,
/// 例如 MediaQuery 这方面的使用等
void main => runApp(FlutterApp);

class FlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: _buildWidgetForNativeRoute(window.defaultRouteName),
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Color(0XFF008577),
accentColor: Color(0xFFD81B60),
primaryColorDark: Color(0xFF00574B),
iconTheme: IconThemeData(color: Color(0xFFD81B60)),
),
);
}
}

/// 该方法用于判断原生界面传递过来的路由值,加载不同的页面
Widget _buildWidgetForNativeRoute(String route) {
switch (route) {
case 'route_flutter':
return GreetFlutterPage;
// 默认的路由值为 '/',所以在 default 情况也需要返回页面,
否则 dart 会报错,这里默认返回空页面
default:
return Scaffold;
}
}

class GreetFlutterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NativeMessageContactPage'),
),
body: Center(
child: Text(
'This is a flutter fragment page',
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
);
}
}

运行后可以看到页面加载出来了,不过会有一段时间的空白,这个在正式打包后就不会出现,所以不必担心。最后的页面应该是这样的

CreateView方式加载

接着看下 createView 方法,说白了,第一种方法最后还是会通过该方式实现

 @Non
public static FlutterView createView(@Non
final Activity activity, @Non final Lifecycle lifecycle, final String initialRoute) {
// 交互前的一些初始化工作,需要完成才可以继续下一步,同时需要保证当前线程为主线程
// Looper.myLooper == Looper.getMainLooper,否则会甩你一脸的 IllegalStateException
FlutterMain.startInitialization(activity.getApplicationContext);
FlutterMain.ensureInitializationComplete(activity.getApplicationContext, );
final FlutterNativeView nativeView = new FlutterNativeView(activity);
// 将 flutter 页面绑定到相应的 activity
final FlutterView flutterView = new FlutterView(activity, , nativeView) {
// ......
};
// 将路由值传到 flutter 层,并加载相应的页面,
if (initialRoute != ) {
flutterView.setInitialRoute(initialRoute);
}

// 绑定 lifecycle,方便生命周期管理,同 activity 绑定
// 不熟悉 LifeCycle 的同学可以自行网上查找资料
lifecycle.addObserver(new LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate {
// 配置一些参数,传递到 flutter 层
final FlutterRunArguments arguments = new FlutterRunArguments;
arguments.bundlePath = FlutterMain
.findAppBundlePath(activity.getApplicationContext);
arguments.entrypoint = "main";
// 最终会调用方法 nativeRunBundleAndSnapshotFromLibrary,这是一个 native 方法,进行交互
flutterView.runFromBundle(arguments);
// 进行注册
GeneratedPluginRegistrant
.registerWith(flutterView.getPluginRegistry);
}
// ......
});

return flutterView;
}

通过 createView 方法返回的 FlutterView,通过设置 Layoutparams 参数就可以添加到相应的布局上,还有一种直接通过 addContentView 方式进行加载,这里直接修改原有代码,

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main) 不需要这一步了
val flutterView = Flutter
.createView(this@ContactActivity, lifecycle, "route_flutter")
val lp = FrameLayout
.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp) // 直接加载到 activity 页面
}

但是通过这样加载的话,那么整个页面都是 flutter 的页面。那么之前的效果的 FAB 则不会被加载出来了,即使没有省略 setContentView(R.layout.activity_main)方法,这个页面的 xml 布局也会被覆盖。

PlantformChannel

那么能够在原生界面显示 flutter 页面了,如何互相交互呢,这就需要通过 PlantformChannel 来执行了,PlantformChannel 主要有三种类型,BasicMessageChannel,MethodChannel,EventChannel。通过查看源码可以发现,三个 Channel 的实现机制类似,都是通过 BinaryMessenger 进行信息交流,每个 Channel 通过传入的 channel name 进行区分,所以在注册 Channel 的时候必须要保证 channel name 是唯一的,同时需要传入一个 BinaryMessageHandler 实例,用于传递信息的处理,当 Handler 处理完信息后,会返回一个 result,然后通过 BinaryMessenger 将 result 返回到 Flutter 层。如果需要深入理解这边推荐一篇文章深入理解Flutter PlatformChannel

接下来直接看例子吧,在创建 PlatformChannel 的时候需要传入一个 BinaryMessenger 实例,通过查看 FlutterView 的源码可以发现,FlutterView 就是一个 BinaryMessenger 在 Android 端的实现,所以呢,可以直接通过前面介绍的 Flutter.createView方法获取注册 Channel 时的 BinaryMessenger 实例了,真是得来全部费工夫~因为通信的方法可能在多个界面会使用,所以还是封装一个通用类来处理会比较合理

BaseMessageChannel

BasicMessageChannel 用于传递字符串和半结构化的信息。

class FlutterPlugin(private val flutterView: FlutterView) :BasicMessageChannel.MessageHandler<Any>{
companion object {
private const val TAG = "FlutterPlugin"

@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
// channel name 需要保持两侧一致
val messageChannel =
BasicMessageChannel(flutterView,
Constant.MESSAGE_CHANNEL_NAME, StandardMessageCodec.INSTANCE) // MessageCodec 有多种实现方式,可以参考推荐的文章

val instance = FlutterPlugin(flutterView)
messageChannel.setMessageHandler(instance) // 注册处理的 Hnadler

return instance
}
}

override fun onMessage(`object`: Any?,
reply: BasicMessageChannel.Reply<Any>?) {
// 简单的将从 Flutter 传过来的消息进行吐司,同时返回自己的交互信息
// `object` 中包含的就是 Flutter 层传递过来的信息,
reply 实例用于传递信息到 Flutter 层
Toast.makeText(flutterView.context, `object`.toString,
Toast.LENGTH_LONG).show
reply?.reply("\"Hello Flutter\"--- an message from Android")
}
}

接着就需要有个 FlutterView 用来注册,新建一个 Activity,用于加载 Flutter 页面

class ContactActivity : AppCompatActivity {
private lateinit var plugin: FlutterPlugin

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 传入路由值,需要在 flutter 层生成相应的界面
val flutterView = Flutter
.createView(this@ContactActivity, lifecycle, "route_contact")
val lp = FrameLayout
.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp)

plugin = FlutterPlugin.registerPlugin(flutterView)
}

override fun onDestroy {
super.onDestroy
}
}

那么我们就要在 Flutter 界面的 _buildWidgetForNativeRoute方法加入新路由值对应的界面

Widget _buildWidgetForNativeRoute(String route) {
switch (route) {
// ...

case 'route_contact':
return FlutterContactPage;

default:
return Scaffold;
}
}

class FlutterContactPage extends StatelessWidget {
// 注册对应的 channel,要保证 channel name 和原生层是一致的
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Page'),
),
// 简单放一个按钮,通过 channel 传输消息过去,同时将原生层返回的消息打印出来
body: RaisedButton(
onPressed: {
_messageChannel
.send('"Hello Native" --- an message from flutter')
.then((str) {
print('Receive message: $str');
});
},
child: Text('Send Message to Native'),
),
);
}
}

最后的效果小伙伴可以自行执行,点击按钮后会弹出吐司,吐司内容就是 Flutter 传递的信息,同时在控制台可以看到从原生层返回的信息。

MethodChannel

MethodChannel 用于传递方法调用(method invocation)

直接在上述例子中进行修改,例如在 Flutter 页面中实现 Activity 的 finish 方法,并传递参数到前一个界面,先做 Flutter 页面的修改,在 AppBar 上增加一个返回按钮,用于返回上层页面

class FlutterContactPage extends StatelessWidget {
// 注册对应的 channel,要保证 channel name 和原生层是一致的
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec);
final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Icon(Icons.arrow_back),
),
onTap: {
_methodChannel
// invokeMethod 第一个值用于传递方法名,第二个值用于传递参数,
// 这边简单的传递一个字符串,当然也可以传递别的类型,map,list 等等
.invokeMethod<bool>('finishActivity', 'Finish Activity')
.then((result) {
// 这边会返回一个结果值,通过判断是否成功来打印不同的信息
print('${result ? 'has finish' : 'not finish'}');
});
},
),
title: Text('Flutter Page'),
),

body: // ...
);
}
}

同时,我们需要在 FlutterPlugin 这个类中,做些必要的修改,首先需要实现 MethodCallHandler接口,该接口中需要实现onMethodCall方法,通过获取调用的方法名和参数值,进行相应的处理

class FlutterPlugin(private val flutterView: FlutterView) :
MethodChannel.MethodCallHandler, BasicMessageChannel.MessageHandler<Any> {

companion object {
private const val TAG = "FlutterPlugin"

@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
val instance = FlutterPlugin(flutterView)
val methodChannel = MethodChannel(flutterView,
Constant.METHOD_CHANNEL_NAME)
// ...
messageChannel.setMessageHandler(instance)
return instance
}
}

// ....

// call 中携带了 Flutter 层传递过来的方法名和参数信息
// 可以分别通过 call.method 和 call.arguments 来获取
override fun onMethodCall(call: MethodCall?, result: MethodChannel.Result?) {
when (call?.method) {
"finishActivity" -> {
val activity = flutterView.context as Activity
val info = call.arguments.toString

val intent = Intent.apply {
putExtra("info", info)
}

activity.setResult(Activity.RESULT_OK, intent)
activity.finish

// 成功时候通过 result.success 返回值,
// 如果发生异常,通过 result.error 返回异常信息
// Flutter 通过 invokeMethod.then 来处理正常结束的逻辑
// 通过 catchError 来处理发生异常的逻辑
result?.success(true)
}

// 如果未找到对应的方法名,则通过 result.notImplemented 来返回异常
else -> result?.notImplemented
}
}

最终的效果,当点击返回按钮的时候,会将 Flutter 层通过 invokeMethod 传递的 arguments 属性吐司出来,同时,控制台会打印出 "has finish" 的信息

EventChannel

EventChannel 用于数据流(event streams)的通信

EventChannel 的实现方式也类似,EventChannel 可以持续返回多个信息到 Flutter 层,在 Flutter 层的表现就是一个 stream,原生层通过 sink 不断的添加数据,Flutter 层接收到数据的变化就会作出新相应的处理。在 Android 端实现状态的监听可以通过广播来实现。直接看例子,还是修改上述代码

class FlutterPlugin(private val flutterView: FlutterView) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler, BasicMessageChannel.MessageHandler<Any> {

private var mStateChangeReceiver: BroadcastReceiver? =

companion object {
private const val TAG = "FlutterPlugin"
const val STATE_CHANGE_ACTION = "com.demo.plugins.action.StateChangeAction"
const val STATE_VALUE = "com.demo.plugins.value.StateValue"

@JvmStatic
fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
// ...
val streamChannel = EventChannel(flutterView,
Constant.STREAM_CHANNEL_NAME)

val instance = FlutterPlugin(flutterView)
methodChannel.setMethodCallHandler(instance)
streamChannel.setStreamHandler(instance)
messageChannel.setMessageHandler(instance)

return instance
}
}

// 实现 StreamHandler 需要重写 onListen 和 onCancel 方法
// onListen 不会每次数据改变就会调用,只在 Flutter 层,eventChannel 订阅广播
// 的时候调用,当取消订阅的时候则会调用 onCancel,
// 所以当开始订阅数据的时候,注册接收数据变化的关闭,
// 在取消订阅的时候,将注册的广播注销,防止内存泄漏
override fun onListen(argument: Any?, sink: EventChannel.EventSink?) {
mStateChangeReceiver = createEventListener(sink)
flutterView.context.registerReceiver(mStateChangeReceiver,
IntentFilter(STATE_CHANGE_ACTION))
}

override fun onCancel(argument: Any?) {
unregisterListener
}

// 在 activity 被销毁的时候,FlutterView 不一定会调用销毁生命周期,
或者会延时调用
// 这就需要手动去注销一开始注册的广播了
fun unregisterListener {
if (mStateChangeReceiver != ) {
flutterView.context.unregisterReceiver(mStateChangeReceiver)
mStateChangeReceiver =
}
}

private fun createEventListener(sink: EventChannel.EventSink?):
BroadcastReceiver = object : BroadcastReceiver {

override fun onReceive(context: Context?, intent: Intent?) {
if (TextUtils.equals(intent?.action, STATE_CHANGE_ACTION)) {
// 这边广播只做简单的接收一个整数,然后通过 sink 传递到 Flutter 层
// 当然,sink 还有 error 方法,用于传递发生的错误信息,
// 以及 endOfStream 方法,用于结束接收
// 在 Flutter 层分别有 onData 对应 success 方法,onError 对应 error 方法
// onDone 对应 endOfStream 方法,根据不同的回调处理不同的逻辑
sink?.success(intent?.getIntExtra(STATE_VALUE, -1))
}
}
}
}

在 Flutter 层,通过对 stream 的监听,对返回的数据进行处理,为了体现出变化,这边修改成 SatefulWidget 来存储状态

class FlutterContactPage extends StatefulWidget {
@override
_FlutterContactPageState createState => _FlutterContactPageState;
}

class _FlutterContactPageState extends State<FlutterContactPage> {
final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);
final EventChannel _eventChannel = EventChannel(STREAM_CHANNEL_NAME);
final BasicMessageChannel _messageChannel =
BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec);
StreamSubscription _subscription;
var _receiverMessage = 'Start receive state'; // 初始的状态值

@override
void initState {
super.initState;
// 当页面生成的时候就开始监听数据的变化
_subscription = _eventChannel.receiveBroadcastStream.listen((data) {
setState( {
_receiverMessage = 'receive state value: $data'; // 数据变化了,则修改数据
});
}, onError: (e) {
_receiverMessage = 'process error: $e'; // 发生错误则显示错误信息
}, onDone: {
_receiverMessage = 'receive data done'; // 发送完毕则直接显示完毕
}, cancelOnError: true);
}

@override
void dispose {
super.dispose;
_subscription.cancel; // 当页面销毁的时候需要将订阅取消,防止内存泄漏
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Icon(Icons.arrow_back),
),
onTap: {
// MethodChannel demo
_methodChannel
.invokeMethod<bool>('finishActivity', _receiverMessage)
.then((result) {
print('${result ? 'has finish' : 'not finish'}');
}).catchError((e) {
print('error happend: $e');
});
},
),
title: Text('Flutter Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
// EventChannel demo,页面直接显示信息的变化
child: Text(
_receiverMessage,
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
// BasicMessageChannel demo
RaisedButton(
onPressed: () {
_messageChannel
.send('"Hello Native" --- an message from flutter')
.then((str) {
print('Receive message: $str');
});
},
child: Text('Send Message to Native'),
),
],
),
),
);
}
}

同时,需要在 Activity 层调用一个定时任务不断的发送广播

class ContactActivity : AppCompatActivity {

private var timer: Timer? =
private var task: TimerTask? =
private lateinit var random: Random
private lateinit var plugin: FlutterPlugin

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

random = Random // 生成随机整数
val flutterView = Flutter.createView(this@ContactActivity, lifecycle,
"route_contact")
val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT)
addContentView(flutterView, lp)

plugin = FlutterPlugin.registerPlugin(flutterView)

timer = Timer // 定时器
task = timerTask { // 定时任务
sendBroadcast(Intent(FlutterPlugin.STATE_CHANGE_ACTION).apply {
putExtra(FlutterPlugin.STATE_VALUE, random.nextInt(1000))
})
}
timer?.schedule(task, 3000, 2000)
// 延时 3s 开启定时器,并 2s 发送一次广播
}

override fun onDestroy {
super.onDestroy

// 页面销毁的时候需要将定时器,定时任务销毁
// 同时注销 Plugin 中注册的广播,防止内存泄漏
timer?.cancel
timer =

task?.cancel
task =

plugin.unregisterListener
}
}

最后的实现效果大概是这样的

Flutter 同 Android 端的交互到这讲的差不多了,和 iOS 的交互其实也类似,只不过在 Android 端通过 FlutterNativeView 来作为 Binarymessenger 的实现,在 iOS 端通过 FlutterBinaryMessenger 协议实现,原理是一致的。至于 Flutter 插件,其实现也是通过以上三种交互方式来实现的,可能我们目前通过 FlutterView 来作为 BinaryMessenger 实例,插件会通过 PluginRegistry.Registrar 实例的 messenger 方法来获取 BinaryMessenger 实例。

需要了解插件的写法也可以直接查看官方提供的检测电量插件:

Flutter Battery Plugin: https://github.com/flutter/plugins/tree/master/packages/battery

往期Flutter系列文,保你一周掌握!

  • Flutter从配置安装到填坑指南详解

  • Flutter 入门指北之 Dart

  • Flutter 入门指北之基础部件

  • Flutter 入门指北之快速搭建界面

  • Flutter 入门指北之常用布局

  • Flutter 入门指北之路由

  • Flutter 入门指北之输入处理(登录界面实战)

  • Flutter 入门指北之滑动部件(超详细)

  • Flutter入门指北之Sliver 组件及NestedScrollView

  • Flutter 入门指北之弹窗和提示(干货)

  • Flutter 入门指北之手势处理和动画

  • Flutter 入门指北之数据持久化

  • Flutter入门指北(Part 13)之网络

  • Flutter入门指北系列最终之实战篇

今日问题:

Flutter搞起来了吗?

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...