Flutter开发者必备面试问题与答案04
视频
https://www.bilibili.com/video/BV1zqynY5E1g/
前言
原文 Flutter 完整面试问题及答案04
本文是 flutter 面试问题的第四讲,高频问答 10 题。
正文
31. as 、show 和 hide 在 import 语句中的区别是什么?
在 Flutter(以及 Dart)中,as
、show
和 hide
是用于 import
语句的关键字,帮助开发者管理命名空间和导入的符号。下面是它们的区别和使用场景:
as
- 功能:用于为导入的库提供一个别名。
使用场景:
- 当你想要避免命名冲突时,可以使用
as
为导入的库指定一个别名。 - 在使用大型库时,使用别名可以让代码更清晰。
- 当你想要避免命名冲突时,可以使用
示例:
import 'package:flutter/material.dart' as myMaterial;
void main() {
runApp(myMaterial.MaterialApp(
home: myMaterial.Scaffold(
appBar: myMaterial.AppBar(title: myMaterial.Text('Hello')),
body: myMaterial.Center(child: myMaterial.Text('Hello World')),
),
));
}
show
- 功能:用于仅导入库中的特定符号,避免导入整个库。
使用场景:
- 当你只需要库中的某几个类或函数时,使用
show
可以减少命名空间的混乱并提高代码的可读性。
- 当你只需要库中的某几个类或函数时,使用
示例:
import 'package:flutter/material.dart' show Text, Scaffold;
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Hello')),
body: Center(child: Text('Hello World')),
),
));
}
hide
- 功能:用于导入库中的所有符号,但排除特定的符号。
使用场景:
- 当你希望导入整个库,但不想使用某些特定的类或函数时,可以使用
hide
。
- 当你希望导入整个库,但不想使用某些特定的类或函数时,可以使用
示例:
import 'package:flutter/material.dart' hide Text;
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Hello')), // 使用 Text 类会报错,因为它被隐藏
body: Center(child: Text('Hello World')), // 这会报错
),
));
}
as
:为导入的库设置别名,避免命名冲突。show
:仅导入库中的特定符号,提高代码的清晰度。hide
:导入库中的所有符号,但排除特定符号,控制命名空间。
根据具体的需求选择适合的导入方式,可以使代码更加整洁、可读和易于维护。
32. TextEditingController 的作用是什么?
在 Flutter 中,TextEditingController
是一个用于管理文本输入的控制器。它主要用于处理 TextField
或 TextFormField
的文本输入,提供了一种方式来读取、修改和清空文本内容。以下是 TextEditingController
的主要作用和使用场景:
主要作用
获取文本内容:
- 通过
TextEditingController
,你可以方便地获取用户在TextField
中输入的文本内容。
- 通过
更新文本内容:
- 你可以使用
TextEditingController
来动态更新TextField
中的文本。例如,可以在某些事件发生时(如按钮点击)更新文本。
- 你可以使用
监听文本变化:
- 通过
TextEditingController
,你可以添加监听器来监控文本的变化,适用于需要实时处理输入的场景。
- 通过
清空文本:
- 可以通过控制器轻松地清空
TextField
中的文本内容。
- 可以通过控制器轻松地清空
使用场景
- 表单输入:在表单中获取用户输入的信息,如用户名、密码等。
- 搜索框:在搜索功能中实时获取用户输入的搜索关键词。
- 动态更新:在用户输入时,实时更新其他 UI 元素的内容或状态。
示例
以下是一个简单的示例,展示如何使用 TextEditingController
:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _controller = TextEditingController();
void _showInput() {
final inputText = _controller.text; // 获取输入文本
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('You entered: $inputText'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
),
],
);
},
);
}
@override
void dispose() {
_controller.dispose(); // 释放控制器资源
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TextEditingController Example')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller, // 设置控制器
decoration: InputDecoration(labelText: 'Enter something'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _showInput,
child: Text('Show Input'),
),
],
),
),
);
}
}
TextEditingController
是在 Flutter 中处理文本输入的强大工具,能够帮助你获取、更新、监听和清空文本内容。它在表单、搜索框和动态输入等场景中非常实用。确保在不再需要控制器时调用 dispose()
方法来释放资源。
33. 为什么我们在 Listview 中使用 Reverse 属性?
在 Flutter 中,使用 ListView
的 reverse
属性可以让列表的滚动方向反转,即从底部开始向顶部滚动。这在某些特定的场景中非常有用,以下是一些常见的使用场景及其原因:
使用场景
聊天应用:
- 在聊天应用中,通常希望最新的消息显示在底部,用户可以向上滚动查看更早的消息。使用
reverse: true
可以使得列表从底部开始滚动,以便用户能够直接看到最新的消息。
- 在聊天应用中,通常希望最新的消息显示在底部,用户可以向上滚动查看更早的消息。使用
时间线或动态列表:
- 类似于社交媒体应用,最新的动态(如评论、点赞)希望在列表的底部,而用户可以向上滚动查看过往的动态。设置
reverse
属性可以实现这一效果。
- 类似于社交媒体应用,最新的动态(如评论、点赞)希望在列表的底部,而用户可以向上滚动查看过往的动态。设置
增强用户体验:
- 通过将列表反转,可以提高用户的交互体验,特别是在需要频繁查看最新内容的场景下,用户无需滚动到底部就能看到新内容。
示例
以下是一个使用 reverse
属性的简单示例:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('ListView Reverse Example')),
body: ListView.builder(
reverse: true, // 反转列表
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${index + 1}'),
);
},
),
),
);
}
}
使用 ListView
的 reverse
属性可以在需要从底部开始显示内容的场景中提高用户体验,尤其是在聊天应用和动态列表中。这种反向滚动的布局方式使得用户可以更方便地查看最新信息,而无需频繁滚动。
34. 模态对话框和持久底部抽屉之间的区别是什么?
在 Flutter 中,模态对话框(Modal Dialog) 和 持久底部抽屉(Persistent Bottom Sheet) 是两种不同的用户界面元素,它们在使用场景、外观和交互方式上有明显的区别。以下是它们的主要区别以及示例说明。
主要区别
模态对话框(Modal Dialog):
- 定义:模态对话框是一个覆盖在应用界面之上的弹出窗口,阻止用户与应用的其他部分进行交互,直到关闭对话框。
- 使用场景:通常用于需要用户确认或输入信息的场景,例如确认删除、输入密码等。
- 外观:通常是一个居中显示的方框,有标题和内容,用户需要采取行动(例如点击按钮)才能关闭它。
持久底部抽屉(Persistent Bottom Sheet):
- 定义:持久底部抽屉是从屏幕底部滑出的面板,可以与用户的主要内容部分同时显示,允许用户与两者交互。
- 使用场景:适合于提供额外选项或操作的场景,比如显示选项列表、附加信息或操作按钮,而不阻止用户与背景内容的交互。
- 外观:通常是一个半透明的面板,从底部滑出,用户可以通过向下滑动或点击背景来关闭它。
示例
模态对话框示例
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Modal Dialog Example')),
body: Center(
child: ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Confirm Action'),
content: Text('Are you sure you want to proceed?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // 关闭对话框
},
child: Text('Cancel'),
),
TextButton(
onPressed: () {
// 执行确认操作
Navigator.of(context).pop();
},
child: Text('OK'),
),
],
);
},
);
},
child: Text('Show Modal Dialog'),
),
),
),
);
}
}
持久底部抽屉示例
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Persistent Bottom Sheet Example')),
body: Center(
child: ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Persistent Bottom Sheet'),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); // 关闭底部抽屉
},
child: Text('Close'),
),
],
),
),
);
},
);
},
child: Text('Show Bottom Sheet'),
),
),
),
);
}
}
- 模态对话框:适用于需要用户确认或输入的场景,阻止与其他界面的交互,通常用于重要的操作。
- 持久底部抽屉:适合提供额外选项或信息,同时允许用户与背景内容交互,通常用于辅助操作。
根据具体的用户交互需求选择合适的组件,可以提升应用的用户体验和可用性。
35. Inherited Widget 与 Provider 有何不同?
在 Flutter 中,InheritedWidget 和 Provider 都是用于在小部件树中传递数据的机制,但它们在使用方式、复杂性和功能上存在一些重要的区别。以下是它们的主要不同点:
定义与目的
InheritedWidget:
- 是 Flutter 框架提供的基础类,允许在小部件树中共享数据。
- 主要用于在小部件树中向下传递数据,适合于简单的状态管理。
- 一般需要手动重建依赖于它的子小部件。
Provider:
- 是一个包(
provider
),基于InheritedWidget
构建的更高级别的状态管理解决方案。 - 提供了更简洁和强大的 API,使状态管理变得更容易和灵活。
- 自动处理依赖关系的重建,简化了状态管理的流程。
- 是一个包(
使用复杂性
InheritedWidget:
- 使用相对复杂,需要手动实现状态的变化和通知。
- 通常需要创建一个自定义的
InheritedWidget
类,管理状态并在状态变化时调用notifyListeners()
。
Provider:
- 使用简便,提供了一种声明式的方式来管理和获取状态。
- 不需要手动实现
InheritedWidget
,可以直接使用ChangeNotifier
和ChangeNotifierProvider
来管理状态。
性能
InheritedWidget:
- 需要手动管理依赖关系,可能导致不必要的重建,尤其是在树中有多个依赖于同一数据的子小部件时。
Provider:
- 提供了更好的性能优化,自动处理依赖关系的重建。只有在相关数据变化时,依赖于该数据的小部件才会重建。
示例
使用 InheritedWidget 示例
import 'package:flutter/material.dart';
class MyInheritedWidget extends InheritedWidget {
final int data;
MyInheritedWidget({Key? key, required this.data, required Widget child})
: super(key: key, child: child);
static MyInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return oldWidget.data != data;
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MyInheritedWidget(
data: 42,
child: Scaffold(
appBar: AppBar(title: Text('InheritedWidget Example')),
body: Center(
child: Text('Data: ${MyInheritedWidget.of(context)!.data}'),
),
),
);
}
}
void main() {
runApp(MaterialApp(home: MyHomePage()));
}
使用 Provider 示例
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MyModel with ChangeNotifier {
int _data = 42;
int get data => _data;
void updateData(int newData) {
_data = newData;
notifyListeners(); // 通知依赖的子小部件重建
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyModel(),
child: Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(
child: Consumer<MyModel>(
builder: (context, model, child) {
return Text('Data: ${model.data}');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<MyModel>().updateData(100); // 更新数据
},
child: Icon(Icons.update),
),
),
);
}
}
void main() {
runApp(MaterialApp(home: MyHomePage()));
}
- InheritedWidget:适合简单的状态管理,使用较为底层,需要手动实现状态和通知。
- Provider:基于
InheritedWidget
,提供更高级和易用的 API,适合中大型应用中的状态管理。
选择使用哪种方式,通常取决于你的应用复杂度和状态管理的需求。对于大多数应用来说,Provider
是推荐的选择,因为它能够简化状态管理并提高代码的可读性。
36. UnmodifiableListView 是什么?
在 Flutter 中,UnmodifiableListView
是一个类,提供了一种不可修改的列表视图。它是 dart:collection
库的一部分,用于封装一个可变的 List
,并确保该列表在创建后无法更改。这意味着你不能添加、删除或修改列表中的元素。
主要特点
不可修改:
- 一旦创建,
UnmodifiableListView
中的元素不能被修改。任何尝试修改操作(如添加、删除或更新元素)都会抛出异常。
- 一旦创建,
适用于暴露内部状态:
- 当你想要暴露一个列表给外部,但又不希望外部代码能够修改这个列表时,可以使用
UnmodifiableListView
。这提供了一种安全的方式来共享数据。
- 当你想要暴露一个列表给外部,但又不希望外部代码能够修改这个列表时,可以使用
视图的同步:
UnmodifiableListView
是对底层可变列表的视图。如果底层列表发生变化,UnmodifiableListView
也会反映这些变化,但外部不能改变它。
使用场景
- 数据保护:在提供 API 或数据类时,确保外部无法直接修改内部状态。
- 返回列表:当返回列表时,希望保持列表不变,可以使用
UnmodifiableListView
。
示例
以下是一个简单的示例,展示如何使用 UnmodifiableListView
:
import 'package:flutter/material.dart';
import 'dart:collection';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final List<String> _items = ['Apple', 'Banana', 'Cherry'];
@override
Widget build(BuildContext context) {
// 使用 UnmodifiableListView 包装可变列表
final unmodifiableList = UnmodifiableListView(_items);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('UnmodifiableListView Example')),
body: ListView.builder(
itemCount: unmodifiableList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(unmodifiableList[index]),
);
},
),
),
);
}
}
注意事项
- 使用
UnmodifiableListView
时,确保底层列表在需要的情况下保持不变,或者在必要时使用其他方式来管理列表的状态。 - 如果需要一个可以修改的列表,应该使用普通的
List
,而不是UnmodifiableListView
。
UnmodifiableListView
提供了一种安全的方式来共享列表数据,防止外部代码对列表进行修改。这在需要保护内部状态的场景中特别有用。
37. 运算符 ?? 和 ?. 之间的区别
在 Flutter(以及 Dart)中,??
和 ?.
是两种不同的运算符,它们在处理 null 值时的行为有所不同。以下是它们的主要区别和使用场景:
1. ??
运算符
- 作用:
??
是 null 合并运算符,用于提供一个默认值。当左侧的表达式为null
时,返回右侧的值;否则,返回左侧的值。 - 使用场景:常用于处理可能为
null
的值,并为其提供一个后备值。
示例:
void main() {
String? name;
String displayName = name ?? 'Guest'; // name 为 null,返回 'Guest'
print(displayName); // 输出: Guest
name = 'Alice';
displayName = name ?? 'Guest'; // name 不为 null,返回 'Alice'
print(displayName); // 输出: Alice
}
2. ?.
运算符
- 作用:
?.
是条件访问运算符,也称为 null 安全访问运算符。用于在访问对象属性或方法时安全地处理null
值。如果左侧的对象为null
,整个表达式将返回null
,而不是抛出异常。 - 使用场景:用于避免因访问
null
对象的属性或方法而引发的运行时错误。
示例:
class User {
String? name;
User(this.name);
}
void main() {
User? user;
print(user?.name); // user 为 null,返回 null
user = User('Alice');
print(user?.name); // user 不为 null,返回 'Alice'
}
??
运算符:- 用于提供默认值,当左侧表达式为
null
时返回右侧值。
- 用于提供默认值,当左侧表达式为
?.
运算符:- 用于安全地访问对象的属性或方法,如果对象为
null
,则整个表达式返回null
,避免运行时错误。
- 用于安全地访问对象的属性或方法,如果对象为
这两个运算符在处理可能的 null 值时非常有用,提高了代码的安全性和可读性。
38. ModalRoute.of() 的目的是什么?
在 Flutter 中,ModalRoute.of(context)
是一个用于获取当前模态路由的静态方法。它的主要目的是在小部件树中查找与给定 BuildContext
相关联的 ModalRoute
实例。这个方法在处理模态对话框、底部抽屉等需要路由管理的场景中非常有用。
主要用途
获取当前路由信息:
ModalRoute
提供了关于当前路由的信息,例如路由的名称、路由的参数等。通过ModalRoute.of(context)
,你可以方便地访问这些信息。
控制路由状态:
- 可以通过获取的
ModalRoute
实例来控制路由的状态,例如使用Navigator
来返回上一个路由或关闭当前模态对话框。
- 可以通过获取的
在小部件中使用路由相关功能:
- 在小部件树中,如果你需要访问当前路由的功能(如获取路由参数或关闭路由),可以通过
ModalRoute.of(context)
来实现。
- 在小部件树中,如果你需要访问当前路由的功能(如获取路由参数或关闭路由),可以通过
示例
以下是一个简单的示例,展示如何使用 ModalRoute.of(context)
:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstPage(),
);
}
}
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Page')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
child: Text('Go to Second Page'),
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取当前的 ModalRoute
final route = ModalRoute.of(context);
return Scaffold(
appBar: AppBar(title: Text('Second Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Current Route: ${route?.settings.name ?? 'Unnamed'}'),
ElevatedButton(
onPressed: () {
Navigator.pop(context); // 返回上一页
},
child: Text('Go Back'),
),
],
),
),
);
}
}
ModalRoute.of(context)
的主要目的是提供对当前模态路由的访问,使得在小部件中能够获取路由信息、控制路由状态以及处理与路由相关的功能。它在构建需要路由管理的应用时非常有用,尤其是在处理对话框和底部抽屉等场景中。
39. Navigator.pushNamed 和 Navigator.pushReplacementNamed 之间的区别是什么?
在 Flutter 中,Navigator.pushNamed
和 Navigator.pushReplacementNamed
都是用于导航到新页面的方法,但它们在路由栈的管理上有显著的区别。以下是这两者的主要区别:
Navigator.pushNamed
- 功能:将新路由推送到当前导航栈中,当前页面保持在栈中。
- 行为:新的页面呈现给用户,用户可以通过后退操作返回到之前的页面。
- 使用场景:适用于需要在多个页面之间来回导航的情况,例如从列表页面导航到详情页面,并允许用户返回。
示例:
Navigator.pushNamed(context, '/detail');
Navigator.pushReplacementNamed
- 功能:将新路由推送到当前导航栈中,同时将当前路由替换掉。
- 行为:新的页面呈现给用户,当前页面被移除,用户无法通过后退操作返回到被替换的页面。
- 使用场景:适用于需要替换当前页面的情况,例如在用户完成某个操作后(如登录后),不希望用户返回到登录页面。
示例:
Navigator.pushReplacementNamed(context, '/home');
Navigator.pushNamed
:- 保留当前页面,允许用户返回。
- 适用于需要在多个页面之间来回导航的场景。
Navigator.pushReplacementNamed
:- 替换当前页面,不允许用户返回。
- 适用于完成操作后需要跳转到新页面的场景,例如登录成功后跳转到主页面。
根据具体的导航需求选择适合的方法,可以提高应用的用户体验和逻辑清晰度。
40. 单实例与作用域实例的区别是什么?
在 Flutter 和 Dart 中,单实例(Singleton)与作用域实例(Scoped Instance)是两种不同的对象管理和创建方式,它们在生命周期、可访问性和使用场景上有显著的区别。以下是它们的主要区别:
1. 单实例(Singleton)
- 定义:单实例是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。
- 生命周期:在应用的整个生命周期内,单实例始终存在,直到应用关闭。
- 可访问性:无论在应用中的哪个地方,你都可以通过同一个访问点获取该实例。
- 使用场景:适用于需要全局状态管理或共享资源的情况,例如配置管理、日志记录、数据库连接等。
示例:
class Singleton {
// 私有构造函数
Singleton._privateConstructor();
// 唯一实例
static final Singleton _instance = Singleton._privateConstructor();
// 获取实例的方法
static Singleton get instance => _instance;
// 示例方法
void someMethod() {
print("This is a singleton method.");
}
}
// 使用
void main() {
var singleton1 = Singleton.instance;
var singleton2 = Singleton.instance;
print(identical(singleton1, singleton2)); // 输出: true
}
2. 作用域实例(Scoped Instance)
- 定义:作用域实例是根据某个特定的上下文或范围创建的实例,通常在特定的生命周期内有效。
- 生命周期:作用域实例的生命周期通常与其所在的作用域(如页面、组件或特定的上下文)相关联,当该作用域结束时,实例也会被销毁。
- 可访问性:作用域实例在其作用域内可访问,通常不跨越不同的作用域。
- 使用场景:适用于需要在特定上下文中共享状态或资源的情况,例如在某个页面中使用的状态管理、依赖注入等。
示例(使用 Provider 作为作用域实例):
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 状态类
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(), // 创建作用域实例
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context); // 访问作用域实例
return Scaffold(
appBar: AppBar(title: Text('Scoped Instance Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: ${counter.count}'),
ElevatedButton(
onPressed: counter.increment,
child: Text('Increment'),
),
],
),
),
);
}
}
单实例(Singleton):
- 在整个应用生命周期内保持唯一,适用于全局状态和资源共享。
作用域实例(Scoped Instance):
- 仅在特定上下文或生命周期内有效,适用于局部状态和资源管理。
根据你的应用需求和架构选择合适的实例管理方式,可以提高代码的可维护性和清晰度。
小结
感谢阅读本文
如果有什么建议,请在评论中让我知道。我很乐意改进。
猫哥 APP
flutter 学习路径
- Flutter 优秀插件推荐
- Flutter 基础篇1 - Dart 语言学习
- Flutter 基础篇2 - 快速上手
- Flutter 实战1 - Getx Woo 电商APP
- Flutter 实战2 - 上架指南 Apple Store、Google Play
- Flutter 基础篇3 - 仿微信朋友圈
- Flutter 实战3 - 腾讯即时通讯 第一篇
- Flutter 实战4 - 腾讯即时通讯 第二篇
© 猫哥
ducafecat.com
end
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。