Flutter Navigator 2(1)

onPressed: () {

Navigator.pushNamed(

context,

‘/details/1’,

);

},

),

),

);

}

}

class DetailScreen extends StatelessWidget {

String id;

DetailScreen({

this.id,

});

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(),

body: Center(

child: Column(

mainAxisAlignment: MainAxisAlignment.center,

children: [

Text(‘Viewing details for item $id’),

FlatButton(

child: Text(‘Pop!’),

onPressed: () {

Navigator.pop(context);

},

),

],

),

),

);

}

}

class UnknownScreen extends StatelessWidget {

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(),

body: Center(

child: Text(‘404!’),

),

);

}

}

这里,我们可以通过 RouteSettings 类型的对象 settings 可以拿到 Navigator.pushNamed 调用时传入的参数。

Navigator 2.0

Navigator 2.0 提供了一系列全新的接口,可以实现将路由状态成为应用状态的一部分,并能够通过底层 API 实现参数解析的功能,新增的 API 如下:

  • Page,用来表示 Navigator 路由栈中各个页面的配置信息。

  • Router,用来制定要由 Navigator 展示的页面列表,通常,该页面列表会根据系统或应用程序的状态改变而改变。

  • RouteInformationParser,持有 RouteInformationProvider 提供的 RouteInformation ,可以将其解析为我们定义的数据类型。

  • RouterDelegate,定义应用程序中的路由行为,例如 Router 如何知道应用程序状态的变化以及如何响应。主要的工作就是监听 RouteInformationParser 和应用状态并通过当前页面列表构建 ·。

  • BackButtonDispatcher,响应后退按钮,并通知 Router

下图展示了 RouterDelegateRouterRouteInformationParser 以及用用状态的交互原理,

大致流程如下:

  1. 当系统打开新页面(如 “books / 2”)时,RouteInformationParser 会将其转换为应用中的具体数据类型 T(如 BooksRoutePath)。

  2. 该数据类型会被传递给 RouterDelegatesetNewRoutePath 方法,我们可以在这里更新路由状态(如通过设置 selectedBookId)并调用 notifyListeners 响应该操作。

  3. notifyListeners 会通知 Router 重建 RouterDelegate(通过 build() 方法).

  4. RouterDelegate.build() 返回一个新的 Navigator 实例,并最终展示出我们想要打开的页面(如 selectedBookId)。

Navigator 2.0 实战

下面,我们就来使用 Navigator 2.0 做一个小小练习,我们将实现一个 Flutter 应用,该应用作用在 Web 上时路由状态会与浏览器中的 URL 连接保持一致,而且也能够处理浏览器的回退按钮,如下:

接下来,使用 flutter channel master 将 Flutter 切换到 master 版本,创建一个支持 Web 应用的 Flutter 项目lib/main.dart 中的代码如下:

import ‘package:flutter/material.dart’;

void main() {

runApp(BooksApp());

}

class Book {

final String title;

final String author;

Book(this.title, this.author);

}

class BooksApp extends StatefulWidget {

@override

State createState() => _BooksAppState();

}

class _BooksAppState extends State {

void initState() {

super.initState();

}

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Books App’,

home: Navigator(

pages: [

MaterialPage(

key: ValueKey(‘BooksListPage’),

child: Scaffold(),

)

],

onPopPage: (route, result) => route.didPop(result),

),

);

}

}

Pages

Navigator 接受一个 pages 参数,如果 Page 列表发生变化,Navigator 也需要更新当前路由栈来保持同步,下面我们就来使用该性质,在新建的项目中开发一个可以展示书单列表的应用

_BooksAppState 中持有两个状态参数:书单列表和当前所选书籍:

class _BooksAppState extends State {

// New:

Book _selectedBook;

bool show404 = false;

List books = [

Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),

Book(‘Foundation’, ‘Isaac Asimov’),

Book(‘Fahrenheit 451’, ‘Ray Bradbury’),

];

// …

然后,在中_BooksAppState,返回一个带有 Page 对象列表的 Navigator :

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Books App’,

home: Navigator(

pages: [

MaterialPage(

key: ValueKey(‘BooksListPage’),

child: BooksListScreen(

books: books,

onTapped: _handleBookTapped,

),

),

],

),

);

}

void _handleBookTapped(Book book) {

setState(() {

_selectedBook = book;

});

}

// …

class BooksListScreen extends StatelessWidget {

final List books;

final ValueChanged onTapped;

BooksListScreen({

@required this.books,

@required this.onTapped,

});

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(),

body: ListView(

children: [

for (var book in books)

ListTile(

title: Text(book.title),

subtitle: Text(book.author),

onTap: () => onTapped(book),

)

],

),

);

}

}

由于此应用会有两个页面(一个书单列表也和一个详情的页面),如果选择了某本书(使用 collection if),则会加入详细页:

pages: [

MaterialPage(

key: ValueKey(‘BooksListPage’),

child: BooksListScreen(

books: books,

onTapped: _handleBookTapped,

),

),

// New:

if (show404)

MaterialPage(key: ValueKey(‘UnknownPage’), child: UnknownScreen())

else if (_selectedBook != null)

MaterialPage(

key: ValueKey(_selectedBook),

child: BookDetailsScreen(book: _selectedBook))

],

注意,这里的 key 会由 book 对象中的值定义作为 MaterialPage 的唯一标识,也就是说,book 对象不同这里的 MaterialPage 就不同。没有唯一的 key,框架就无法确定何时显示不同 Page 之间的过渡动画。

我们还可以继承 Page 来实现自定义行为,例如,在该页面添加了自定义过渡动画:

class BookDetailsPage extends Page {

final Book book;

BookDetailsPage({

this.book,

}) : super(key: ValueKey(book));

Route createRoute(BuildContext context) {

return PageRouteBuilder(

settings: this,

pageBuilder: (context, animation, animation2) {

final tween = Tween(begin: Offset(0.0, 1.0), end: Offset.zero);

final curveTween = CurveTween(curve: Curves.easeInOut);

return SlideTransition(

position: animation.drive(curveTween).drive(tween),

child: BookDetailsScreen(

key: ValueKey(book),

book: book,

),

);

},

);

}

}

还需要注意的是,只传入 pages 参数而不传入 onPopPage 也会报错,他接受一个回调函数,每次 Navigator.pop() 被调用时就会出发这个函数,我们可以在其中更新路由状态

最后,在pages不提供onPopPage回调的情况下提供参数是错误的。每次调用时都会Navigator.pop()调用此函数。应该使用它来更新状态(修改页面列表),这里我们需要调用 didPop 方法确定是否 pop 成功:

onPopPage: (route, result) {

if (!route.didPop(result)) {

return false;

}

// Update the list of pages by setting _selectedBook to null

setState(() {

_selectedBook = null;

});

return true;

},

我们还必须在更新应用程序状态之前检查是否 pop 失败。这里,我们使用了 setState 方法来通知 Flutter 调用 build() 方法,该方法 _selectedBook 为 null 表示展示书单列表页。

完整代码如下:

import ‘package:flutter/material.dart’;

void main() {

runApp(BooksApp());

}

class Book {

final String title;

final String author;

Book(this.title, this.author);

}

class BooksApp extends StatefulWidget {

@override

State createState() => _BooksAppState();

}

class _BooksAppState extends State {

Book _selectedBook;

List books = [

Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),

Book(‘Foundation’, ‘Isaac Asimov’),

Book(‘Fahrenheit 451’, ‘Ray Bradbury’),

];

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Books App’,

home: Navigator(

pages: [

MaterialPage(

key: ValueKey(‘BooksListPage’),

child: BooksListScreen(

books: books,

onTapped: _handleBookTapped,

),

),

if (_selectedBook != null) BookDetailsPage(book: _selectedBook)

],

onPopPage: (route, result) {

if (!route.didPop(result)) {

return false;

}

// Update the list of pages by setting _selectedBook to null

setState(() {

_selectedBook = null;

});

return true;

},

),

);

}

void _handleBookTapped(Book book) {

setState(() {

_selectedBook = book;

});

}

}

class BookDetailsPage extends Page {

final Book book;

BookDetailsPage({

this.book,

}) : super(key: ValueKey(book));

Route createRoute(BuildContext context) {

return MaterialPageRoute(

settings: this,

builder: (BuildContext context) {

return BookDetailsScreen(book: book);

},

);

}

}

class BooksListScreen extends StatelessWidget {

final List books;

final ValueChanged onTapped;

BooksListScreen({

@required this.books,

@required this.onTapped,

});

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(),

body: ListView(

children: [

for (var book in books)

ListTile(

title: Text(book.title),

subtitle: Text(book.author),

onTap: () => onTapped(book),

)

],

),

);

}

}

class BookDetailsScreen extends StatelessWidget {

final Book book;

BookDetailsScreen({

@required this.book,

});

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(),

body: Padding(

padding: const EdgeInsets.all(8.0),

child: Column(

crossAxisAlignment: CrossAxisAlignment.start,

children: [

if (book != null) …[

Text(book.title, style: Theme.of(context).textTheme.headline6),

Text(book.author, style: Theme.of(context).textTheme.subtitle1),

],

],

),

),

);

}

}

目前,我们就实现了声明式的路由管理,单此时我们还无法处理浏览器的后退按钮,也不能同步浏览器地址拦中的链接。

Router

本节,我们来实现通过 RouteInformationParser, RouterDelegate 更新路由状态,实现与浏览器地址拦中的链接同步

数据类型

首先,我们需要通过 RouteInformationParser 将路由信息解析为指定的数据类型:

class BookRoutePath {

final int id;

final bool isUnknown;

BookRoutePath.home()
id = null,

isUnknown = false;

BookRoutePath.details(this.id) : isUnknown = false;

BookRoutePath.unknown()
id = null,

isUnknown = true;

bool get isHomePage => id == null;

bool get isDetailsPage => id != null;

}

在该应用程序中,可以使用 BookRoutePath 类来表示应用程序中的路由路径,我们也可以实现父子类来关系其他各类型的路由信息。

RouterDelegate

接下来,我们实现一个 RouterDelegate 的子类 BookRouterDelegate

class BookRouterDelegate extends RouterDelegate

with ChangeNotifier, PopNavigatorRouterDelegateMixin {

@override

Widget build(BuildContext context) {

// TODO

throw UnimplementedError();

}

@override

// TODO

GlobalKey get navigatorKey => throw UnimplementedError();

@override

Future setNewRoutePath(BookRoutePath configuration) {

// TODO

throw UnimplementedError();

}

}

BookRouterDelegate 的泛型为 BookRoutePath,其中包含了决定显示哪个页面所需的所有状态。

此时,我们就可以将 _BooksAppState 中的路由相关的逻辑放到 BookRouterDelegate 中,这里,我们创建了一个 GlobalKey 对象,其他各个状态也都保存在这里面:

class BookRouterDelegate extends RouterDelegate

with ChangeNotifier, PopNavigatorRouterDelegateMixin {

final GlobalKey navigatorKey;

Book _selectedBook;

bool show404 = false;

List books = [

Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),

Book(‘Foundation’, ‘Isaac Asimov’),

Book(‘Fahrenheit 451’, ‘Ray Bradbury’),

];

BookRouterDelegate() : navigatorKey = GlobalKey();

// …

为了能在 URL 中显示正确的路径,我们也需要根据应用程序的当前状态返回一个 BookRoutePath 对象:

BookRoutePath get currentConfiguration {

if (show404) {

return BookRoutePath.unknown();

}

return _selectedBook == null

? BookRoutePath.home()
BookRoutePath.details(books.indexOf(_selectedBook));

}

下面,build 方法返回一个 Navigator 组件:

@override

Widget build(BuildContext context) {

return Navigator(

key: navigatorKey,

pages: [

MaterialPage(

key: ValueKey(‘BooksListPage’),

child: BooksListScreen(

books: books,

onTapped: _handleBookTapped,

),

),

if (show404)

MaterialPage(key: ValueKey(‘UnknownPage’), child: UnknownScreen())

else if (_selectedBook != null)

BookDetailsPage(book: _selectedBook)

],

onPopPage: (route, result) {

if (!route.didPop(result)) {

return false;

}

// Update the list of pages by setting _selectedBook to null

_selectedBook = null;

show404 = false;

notifyListeners();

return true;

},

);

}

因为该类并不是组件,而是由 ChangeNotifier 实现,因此这里的 onPopPage 方法需要使用 notifyListeners 替代 setState 来改变状态,当 RouterDelegate 触发状态更新时,Router 同样会触发 RouterDelegatecurrentConfiguration 方法并调用 build 方法创建出一个新的 Navigator 组件。

_handleBookTapped方法也需要使用 notifyListeners 代替 setState

void _handleBookTapped(Book book) {

_selectedBook = book;

notifyListeners();

}

新页面打开后,Router 会调用setNewRoutePath 方法来更新应用程序的路由状态:

@override

Future setNewRoutePath(BookRoutePath path) async {

if (path.isUnknown) {

_selectedBook = null;

show404 = true;

return;

}

if (path.isDetailsPage) {

if (path.id < 0 || path.id > books.length - 1) {

show404 = true;

return;

}

_selectedBook = books[path.id];

} else {

_selectedBook = null;

}

show404 = false;

}

RouteInformationParser

RouteInformationParser 内部含有一个钩子函数,接受路由信息(RouteInformation),我们可以在这里将它转换成指定的数据类型(BookRoutePath)并使用 Uri 解析:

class BookRouteInformationParser extends RouteInformationParser {

@override

Future parseRouteInformation(

RouteInformation routeInformation) async {

final uri = Uri.parse(routeInformation.location);

// Handle ‘/’

if (uri.pathSegments.length == 0) {

return BookRoutePath.home();

}

// Handle ‘/book/:id’

if (uri.pathSegments.length == 2) {

if (uri.pathSegments[0] != ‘book’) return BookRoutePath.unknown();

var remaining = uri.pathSegments[1];

var id = int.tryParse(remaining);

if (id == null) return BookRoutePath.unknown();

return BookRoutePath.details(id);

}

// Handle unknown routes

return BookRoutePath.unknown();

}

@override

RouteInformation restoreRouteInformation(BookRoutePath path) {

if (path.isUnknown) {

return RouteInformation(location: ‘/404’);

}

if (path.isHomePage) {

return RouteInformation(location: ‘/’);

}

if (path.isDetailsPage) {

return RouteInformation(location: ‘/book/${path.id}’);

}

return null;

}

}

该实现仅针对此应用,并不是常规的路由解析解决方案,具体原理,我们以后再详细了解。最后,要使用这些定义好的类,我们还需要使用全新的 MaterialApp.router 构造函数并传入它们各自的实现:

return MaterialApp.router(

title: ‘Books App’,

routerDelegate: _routerDelegate,

routeInformationParser: _routeInformationParser,

);

完整代码如下:

import ‘package:flutter/material.dart’;

void main() {

runApp(BooksApp());

}

class Book {

final String title;

final String author;

Book(this.title, this.author);

}

class BooksApp extends StatefulWidget {

@override

State createState() => _BooksAppState();

}

class _BooksAppState extends State {

BookRouterDelegate _routerDelegate = BookRouterDelegate();

BookRouteInformationParser _routeInformationParser =

BookRouteInformationParser();

@override

Widget build(BuildContext context) {

return MaterialApp.router(

title: ‘Books App’,

routerDelegate: _routerDelegate,

routeInformationParser: _routeInformationParser,

);

}

}

class BookRouteInformationParser extends RouteInformationParser {

@override

Future parseRouteInformation(

RouteInformation routeInformation) async {

final uri = Uri.parse(routeInformation.location);

// Handle ‘/’

if (uri.pathSegments.length == 0) {

return BookRoutePath.home();

}

// Handle ‘/book/:id’

if (uri.pathSegments.length == 2) {

if (uri.pathSegments[0] != ‘book’) return BookRoutePath.unknown();

var remaining = uri.pathSegments[1];

var id = int.tryParse(remaining);

if (id == null) return BookRoutePath.unknown();

return BookRoutePath.details(id);

}

// Handle unknown routes

return BookRoutePath.unknown();

}

@override

RouteInformation restoreRouteInformation(BookRoutePath path) {

if (path.isUnknown) {

return RouteInformation(location: ‘/404’);

}

if (path.isHomePage) {

return RouteInformation(location: ‘/’);

}

if (path.isDetailsPage) {

return RouteInformation(location: ‘/book/${path.id}’);

}

return null;

}

}

class BookRouterDelegate extends RouterDelegate

with ChangeNotifier, PopNavigatorRouterDelegateMixin {

final GlobalKey navigatorKey;

Book _selectedBook;

bool show404 = false;

List books = [

Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),

Book(‘Foundation’, ‘Isaac Asimov’),

Book(‘Fahrenheit 451’, ‘Ray Bradbury’),

];

BookRouterDelegate() : navigatorKey = GlobalKey();

BookRoutePath get currentConfiguration {

if (show404) {

return BookRoutePath.unknown();

}

return _selectedBook == null

? BookRoutePath.home()
BookRoutePath.details(books.indexOf(_selectedBook));

}

@override

Widget build(BuildContext context) {

return Navigator(

key: navigatorKey,

pages: [

MaterialPage(

key: ValueKey(‘BooksListPage’),

child: BooksListScreen(

books: books,

onTapped: _handleBookTapped,

),

),

if (show404)

MaterialPage(key: ValueKey(‘UnknownPage’), child: UnknownScreen())

else if (_selectedBook != null)

BookDetailsPage(book: _selectedBook)

],

onPopPage: (route, result) {

if (!route.didPop(result)) {

return false;

}

// Update the list of pages by setting _selectedBook to null

_selectedBook = null;

show404 = false;

notifyListeners();

return true;

},

);

}

@override

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

另外,描述问题一定要慢!不要一下子讲一大堆,慢显得你沉稳、自信,而且你还有时间反应思路接下来怎么讲更好。现在开发过多依赖ide,所以会有个弊端,当我们在面试讲解很容易不知道某个方法怎么读,这是一个硬伤…所以一定要对常见的关键性的类名、方法名、关键字读准,有些面试官不耐烦会说“你到底说的是哪个?”这时我们会容易乱了阵脚。正确的发音+沉稳的描述+好听的嗓音决对是一个加分项!

最重要的是心态!心态!心态!重要事情说三遍!面试时间很短,在短时间内对方要摸清你的底子还是比较不现实的,所以,有时也是看眼缘,这还是个看脸的时代。

希望大家都能找到合适自己满意的工作!

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-jf5TTwGM-1712260118475)]

[外链图片转存中…(img-9aTXd9M9-1712260118475)]

[外链图片转存中…(img-DaEW1nKk-1712260118476)]

[外链图片转存中…(img-aDYTcfZi-1712260118476)]

[外链图片转存中…(img-Pri6mDxm-1712260118476)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

另外,描述问题一定要慢!不要一下子讲一大堆,慢显得你沉稳、自信,而且你还有时间反应思路接下来怎么讲更好。现在开发过多依赖ide,所以会有个弊端,当我们在面试讲解很容易不知道某个方法怎么读,这是一个硬伤…所以一定要对常见的关键性的类名、方法名、关键字读准,有些面试官不耐烦会说“你到底说的是哪个?”这时我们会容易乱了阵脚。正确的发音+沉稳的描述+好听的嗓音决对是一个加分项!

最重要的是心态!心态!心态!重要事情说三遍!面试时间很短,在短时间内对方要摸清你的底子还是比较不现实的,所以,有时也是看眼缘,这还是个看脸的时代。

希望大家都能找到合适自己满意的工作!

进阶学习视频

[外链图片转存中…(img-DmpW0gDJ-1712260118477)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-ISEx5wEw-1712260118477)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值