GitHub标星8-3K,字节跳动大牛在原生项目中集成Flutter,一篇文章搞定!

假设我们的原生应用在 some/path/MyApp 路径下,那么在Flutter 项目的同级目录下新建一个Flutter模块,命令如下。

cd some/path/
flutter create -t module --org com.example my_flutter

完成上面的命令后,会在 some/path/my_flutter/ 目录下创建一个 Flutter 模块项目。该模块项目会包含一些 Dart 代码和一些一个隐藏的子文件夹 .android/,.android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

同时,由于Flutter Android 引擎需要使用到 Java 8 中的新特性。因此,需要在宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性代码。

android {
//…
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

接下来,需要将Flutter module添加到原生Android工程的依赖中。将 Flutter 模块添加到原生Android应用程序中主要有两种方法实现。使用AAR包方式和直接使用module源码的方式。使用AAR包方式需要先将Flutter 模块打包成AAR包。假设,你的 Flutter 模块在 some/path/my_flutter 目录下,那么打包AAR包的命令如下。

cd some/path/my_flutter
flutter build aar

然后,根据屏幕上的提示完成集成操作,如下图所示,当然也可以在Android原生工程中进行手动添加依赖代码。

事实上,该命令主要用于创建(默认情况下创建 debug/profile/release 所有模式)本地存储库,主要包含以下文件,如下所示。

build/host/outputs/repo
└── com
└── example
└── my_flutter
├── flutter_release
│ ├── 1.0
│ │ ├── flutter_release-1.0.aar
│ │ ├── flutter_release-1.0.aar.md5
│ │ ├── flutter_release-1.0.aar.sha1
│ │ ├── flutter_release-1.0.pom
│ │ ├── flutter_release-1.0.pom.md5
│ │ └── flutter_release-1.0.pom.sha1
│ ├── maven-metadata.xml
│ ├── maven-metadata.xml.md5
│ └── maven-metadata.xml.sha1
├── flutter_profile
│ ├── …
└── flutter_debug
└── …

可以发现,使用上面的命令编译的AAR包主要分为debug、profile和release三个版本,使用哪个版本的AAR需要根据原生的环境进行选择。找到AAR包,然后再Android宿主应用程序中修改 app/build.gradle 文件,使其包含本地存储库和上述依赖项,如下所示。

android {
// …
}

repositories {
maven {
url ‘some/path/my_flutter/build/host/outputs/repo’
// This is relative to the location of the build.gradle file
// if using a relative path.
}
maven {
url ‘https://storage.googleapis.com/download.flutter.io’
}
}

dependencies {
// …
debugImplementation ‘com.example.flutter_module:flutter_debug:1.0’
profileImplementation ‘com.example.flutter_module:flutter_profile:1.0’
releaseImplementation ‘com.example.flutter_module:flutter_release:1.0’
}

当然,除了命令方式外,还可以使用Android Studio来构建AAR包。依次点击 Android Studio 菜单中的 Build > Flutter > Build AAR 即可构建Flutter 模块的 AAR包,如下图所示。

除了AAR包方式外,另一种方式就是使用源码的方式进行依赖,即将flutter_module模块作为一个模块添加到Android原生工程中。首先,将Flutter 模块作为子项目添加到宿主应用的 settings.gradle 中,如下所示。

// Include the host app project.
include ‘:app’
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
‘my_flutter/.android/include_flutter.groovy’
))

binding 和 evaluation 脚本可以使 Flutter 模块将其自身(如 :flutter)和该模块使用的所有 Flutter 插件(如 :package_info,:video_player 等)都包含在 settings.gradle 上下文中,然后在原生Android工程的app目录下的build.gradle文件下添加如下依赖代码。

dependencies {
implementation project(‘:flutter’)
}

到此,在原生Android工程中集成Flutter环境就完成了,接下来编写代码即可。

添加Flutter页面

正常跳转

1, 添加FlutterActivity
Flutter提供了一个FlutterActivity来作为Flutter的容器页面,FlutterActivity和Android原生的Activity没有任何区别,可以认为它是Flutter的父容器组件,但在原生Android程序中,它就是一个普通的Activity,这个Activity必须在AndroidManifest.xml中进行注册,如下所示。

对于theme属性,我们可以使用Android的其他样式进行替换,此主题样式会决定了应用的系统样式。

2,打开FlutterActivity

在AndroidManifest.xml中注册FlutterActivity后,然后我们可以在任何地方启动这个FlutterActivity,如下所示。

myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(MainActivity.this)
);
}
});

运行上面的代码,发现并不会跳转到Flutter页面,因为我们并没有提供跳转的地址。下面的示例将演示如何使用自定义路由跳转到Flutter模块页面中,如下所示。

myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute(“/my_route”)
.build(currentActivity)
);
}
});

其中,my_route为Flutter模块的初始路由,关于Flutter的路由知识,可以看下面的文章:Flutter开发之路由与导航

我们使用withNewEngine()工厂方法配置,创建一个的FlutterEngine实例。当运行上面的代码时,应用就会由原生页面跳转到Flutter模块页面。

3,使用带有缓存的FlutterEngine

每个FlutterActivity在默认情况下都会创建自己的FlutterEngine,并且每个FlutterEngine在启动时都需要有一定的预热时间。这意味着在原生页面跳转到Flutter模块页面之前会一定的时间延迟。为了尽量减少这个延迟,你可以在启动Flutter页面之前先预热的FlutterEngine。即在应用程序中运行过程中找一个合理的时间实例化一个FlutterEngine,如在Application中进行初始化,如下所示。

public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
flutterEngine = new FlutterEngine(this);

flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);

FlutterEngineCache.getInstance().put(“my_engine_id”, flutterEngine);
}
}

其中,FlutterEngineCache的ID可以是任意的字符串,使用时请确保传递给任何使用缓存的FlutterEngine的FlutterFragment或FlutterActivity使用的是相同的ID。完成上面的自定义Application后,我们还需要在原生Android工程的AndroidManifest.xml中使用自定义的Application,如下所示。


下面我们来看一下如何在FlutterActivity页面中使用缓存的FlutterEngine,现在使用FlutterActivity跳转到Flutter模块时需要使用上面的ID,如下所示。

myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine(“my_engine_id”)
.build(currentActivity)
);
}
});

可以发现,在使用withCachedEngine()工厂方法后,打开Flutter模块的延迟时间大大降低了。

4,使用缓存引擎的初始路由
当使用带有FlutterEngine配置的FlutterActivity或者FlutterFragment时,会有初始路由的概念,我们可以在代码中添加跳转到Flutter模块的初始路由。然而,当我们使用带有缓存的FlutterEngine时,FlutterActivity和FlutterFragment并没有提供初始路由的概念。如果开发人员希望使用带有缓存的FlutterEngine时也能自定义初始路由,那么可以在执行Dart入口点之前配置他们的缓存FlutterEngine以使用自定义初始路由,如下所示。

public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute(“your/route/here”);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);

FlutterEngineCache
.getInstance()
.put(“my_engine_id”, flutterEngine);
}
}

带有背景样式的跳转

如果要修改跳转的样式,那么可以在原生Android端自定义一个主题样式呈现一个半透明的背景。首先打开res/values/styles.xml文件,然后添加自定义的主题,如下所示。

然后,将FlutterActivity的主题改为我们自定义的主题,如下所示。

然后,就可以使用透明背景启动FlutterActivity,如下所示。

// Using a new FlutterEngine.
startActivity(
FlutterActivity.withNewEngine()
.backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
.build(context)
);

// Using a cached FlutterEngine.
startActivity(
FlutterActivity.withCachedEngine(“my_engine_id”)
.backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
.build(context)
);

添加FlutterFragment

在Android开发中,除了Activity之外,还可以使用Fragment来加载页面,Fragment比Activity的粒度更小,有碎片化的意思。如果有碎片化加载的场景,那么可以使用FlutterFragment 。FlutterFragment允许开发者控制以下操作:

  • 初始化Flutter的路由;
  • Dart的初始页面的飞入样式;
  • 设置不透明和半透明背景;
  • FlutterFragment是否可以控制Activity;
  • FlutterEngine或者带有缓存的FlutterEngine是否能使用;

1,将FlutterFragment 添加到Activity
使用FlutterFragment要做的第一件事就是将其添加到宿主Activity中。为了给宿主Activity添加一个FlutterFragment,需要在Activity的onCreate()中实例化并附加一个FlutterFragment的实例,这和原生Android的Fragment使用方法是一样的,代码如下:

public class MyActivity extends FragmentActivity {

private static final String TAG_FLUTTER_FRAGMENT = “flutter_fragment”;
private FlutterFragment flutterFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity_layout);
FragmentManager fragmentManager = getSupportFragmentManager();
flutterFragment = (FlutterFragment) fragmentManager
.findFragmentByTag(TAG_FLUTTER_FRAGMENT);

if (flutterFragment == null) {
flutterFragment = FlutterFragment.createDefault();
fragmentManager
.beginTransaction()
.add( R.id.fragment_container, flutterFragment, TAG_FLUTTER_FRAGMENT )
.commit();
}
}
}

其中,代码中用到的原生Fragment的布局代码如下所示。

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.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”>

</androidx.constraintlayout.widget.ConstraintLayout>

然后,将原生Android的启动页面改为我们的MyActivity即可。除此之外,我们还可以借助FlutterFragment来获取原生代码的生命周期,并作出相关的逻辑操作,如下所示。

public class MyActivity extends FragmentActivity {
@Override
public void onPostResume() {
super.onPostResume();
flutterFragment.onPostResume();
}

@Override
protected void onNewIntent(@NonNull Intent intent) {
flutterFragment.onNewIntent(intent);
}

@Override
public void onBackPressed() {
flutterFragment.onBackPressed();
}

@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
flutterFragment.onRequestPermissionsResult(
requestCode,
permissions,
grantResults
);
}

@Override
public void onUserLeaveHint() {
flutterFragment.onUserLeaveHint();
}

@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
flutterFragment.onTrimMemory(level);
}
}

不过,上面的示例启动时使用了一个新的FlutterEngine,因此启动后会需要一定的初始化时间,导致应用启动后会有一个空白的UI,直到FlutterEngine初始化成功后Flutter模块的首页渲染完成。对于这种现象,我们同样可以在提前初始化FlutterEngine,即在应用程序的Application中初始化FlutterFragment,如下所示。

public class MyApplication extends Application {

FlutterEngine flutterEngine=null;

@Override
public void onCreate() {
super.onCreate();
flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute(“your/route/here”);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
FlutterEngineCache
.getInstance()
.put(“my_engine_id”, flutterEngine);
}
}

在上面的代码中,通过设置导航通道的初始路由,然后关联的FlutterEngine在初始执行runApp() ,在初始执行runApp()后再改变导航通道的初始路由属性是没有效果的。然后,我们修改MyFlutterFragmentActivity类的代码,并使用FlutterFragment.withNewEngine()使用缓存的FlutterEngine,如下所示。

public class MyFlutterFragmentActivity extends FragmentActivity {

private static final String TAG_FLUTTER_FRAGMENT = “flutter_fragment”;
private FlutterFragment flutterFragment = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.flutter_fragment_activity);
FragmentManager fragmentManager = getSupportFragmentManager();
if (flutterFragment == null) {
flutterFragment=FlutterFragment.withNewEngine()
.initialRoute(“/”)
.build();

fragmentManager
.beginTransaction()
.add(R.id.fragment_container, flutterFragment,TAG_FLUTTER_FRAGMENT)
.commit();
}
}
}

控制FlutterFragment的渲染模式

FlutterFragment默认使用SurfaceView来渲染它的Flutter内容,除此之外,还可以使用TextureView来渲染界面,不过SurfaceView的性能比TextureView好得多。但是,SurfaceView不能交错在Android视图层次结构中使用。此外,在Android N之前的Android版本中,SurfaceViews不能动画化,因为它们的布局和渲染不能与其他视图层次结构同步,此时,你需要使用TextureView而不是SurfaceView,使用 TextureView来渲染FlutterFragment的代码如下。

// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.renderMode(FlutterView.RenderMode.texture)
.build();

// With a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine(“my_engine_id”)
.renderMode(FlutterView.RenderMode.texture)
.build();

如果要给跳转添加一个转场的透明效果,要启用FlutterFragment的透明属性,可以使用下面的配置,如下所示。

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.transparencyMode(TransparencyMode.transparent)
.build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine(“my_engine_id”)
.transparencyMode(TransparencyMode.transparent)
.build();

FlutterFragment 与Activity

有时候,一些应用使用Fragment来作为Flutter页面的承载对象时,状态栏、导航栏和屏幕方向仍然使用的是Activity,Fragment只是作为Activity的一部分。在这些应用程序中,用一个Fragment是合理的,如下图所示。
在其他应用程序中,Fragment仅仅作为UI的一部分,此时一个FlutterFragment可能被用来实现一个抽屉的内部,一个视频播放器,或一个单一的卡片。在这些情况下,FlutterFragment不需要全屏线上,因为在同一个屏幕中还有其他UI片段,如下图所示。

FlutterFragment提供了一个概念,用来实现FlutterFragment是否能够控制它的宿主Activity。为了防止一个FlutterFragment将它的Activity暴露给Flutter插件,也为了防止Flutter控制Activity的系统UI,FlutterFragment提供了一个shouldAttachEngineToActivity()方法,如下所示。

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.shouldAttachEngineToActivity(false)
.build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine(“my_engine_id”)
.shouldAttachEngineToActivity(false)
.build();

原生iOS集成Flutter

创建Flutter模块

为了将 Flutter 集成到原生iOS应用里,第一步要创建一个 Flutter module,创建 Flutter module的命令如下所示。

cd some/path/
flutter create --template module my_flutter

执行完上面的命令后,会在some/path/my_flutter/ 目录下创建一个Flutter module库。在这个目录中,你可以像在其它 Flutter 项目中一样,执行 flutter 命令,比如 flutter run --debug 或者 flutter build ios。打开 my_flutter 模块,可以发现,目录结构和普通 的Flutter 应用的目录别无二至,如下所示。

my_flutter/
├── .ios/
│ ├── Runner.xcworkspace
│ └── Flutter/podhelper.rb
├── lib/
│ └── main.dart
├── test/
└── pubspec.yaml

默认情况下,my_flutter的Android工程和iOS工程是隐藏的,我们可以通过显示隐藏的项目来看到Android工程和iOS工程。

集成到已有iOS应用

在原生iOS开发中,有两种方式可以将 Flutter 集成到你的既有应用中。
1, 使用 CocoaPods 依赖管理和已安装的 Flutter SDK 。(推荐)
2,把 Flutter engine 、Dart 代码和所有 Flutter plugin 编译成 framework,然后用 Xcode 手动集成到你的应用中,并更新编译设置。

1, 使用 CocoaPods 和 Flutter SDK 集成

使用此方法集成Flutter,需要在本地安装了 Flutter SDK。然后,只需要在 Xcode 中编译应用,就可以自动运行脚本来集成Dart 代码和 plugin。这个方法允许你使用 Flutter module 中的最新代码快速迭代开发,而无需在 Xcode 以外执行额外的命令。

现在假如又一个原生iOS工程,并且 Flutter module 和这个iOS工程是处在相邻目录的,如下所示。

some/path/
├── my_flutter/
│ └── .ios/
│ └── Flutter/
│ └── podhelper.rb
└── MyApp/
└── Podfile

1,如果你的应用(MyApp)还没有 Podfile,可以根据 CocoaPods 使用指南 来在项目中添加 Podfile。然后,在 Podfile 中添加下面代码:

flutter_application_path = ‘…/my_flutter’
load File.join(flutter_application_path, ‘.ios’, ‘Flutter’, ‘podhelper.rb’)

2,每个需要集成 Flutter 的 [Podfile target][],执行 install_all_flutter_pods(flutter_application_path),如下所示。

target ‘MyApp’ do
install_all_flutter_pods(flutter_application_path)
end

3,最后,在MyApp原生工程下运行 pod install命令拉取原生工程需要的插件。

pod install

如果没有任何错误,界面如下图。

在上面的Podfile文件中, podhelper.rb 脚本会把你的 plugins, Flutter.framework,和 App.framework 集成到你的原生iOS项目中。同时,你应用的 Debug 和 Release 编译配置,将会集成相对应的 Debug 或 Release 的 编译产物。可以增加一个 Profile 编译配置用于在 profile 模式下测试应用。然后,在 Xcode 中打开 MyApp.xcworkspace ,可以使用 【⌘B 】快捷键编译项目,并运行项目即可。

使用frameworks集成

除了上面的方法,你也可以创建一个 frameworks,手动修改既有 Xcode 项目,将他们集成进去。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework来编译framework。下面的示例假设你想在 some/path/MyApp/Flutter/ 目录下创建 frameworks。

flutter build ios-framework --output=some/path/MyApp/Flutter/

此时的文件目录如下所示。

some/path/MyApp/
└── Flutter/
├── Debug/
│ ├── Flutter.framework
│ ├── App.framework
│ ├── FlutterPluginRegistrant.framework (only if you have plugins with iOS platform code)
│ └── example_plugin.framework (each plugin is a separate framework)
├── Profile/
│ ├── Flutter.framework
│ ├── App.framework
│ ├── FlutterPluginRegistrant.framework
│ └── example_plugin.framework
└── Release/
├── Flutter.framework
├── App.framework
├── FlutterPluginRegistrant.framework
└── example_plugin.framework

然后,使用 Xcode 打开原生iOS工程,并将生成的 frameworks 集成到既有iOS应用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。

1, 链接到框架

当然,你也可以将框架从 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目标项目中,然后点击 build settings > Build Phases > Link Binary With Libraries。然后,在 target 的编译设置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/,如下图所示。

2,内嵌框架
生成的动态framework框架必须嵌入你的应用才能在运行时被加载。需要说明的是插件会帮助你生成 静态或动态框架。静态框架应该直接链接而不是嵌入,如果你在应用中嵌入了静态框架,你的应用将不能发布到 App Store 并且会得到一个 Found an unexpected Mach-O header code 的 archive 错误。

你可以从应用框架组中拖拽框架(除了 FlutterPluginRegistrant 以及其他的静态框架)到你的目标 ‘ build settings > Build Phases > Embed Frameworks,然后从下拉菜单中选择 “Embed & Sign”,如下图所示。

3,使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用

除了使用Flutter.framework方式外,你还可以加入一个参数 --cocoapods ,然后将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件,命令如下。

flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/

执行命令后,Flutter模块的目录如下图所示。

some/path/MyApp/
└── Flutter/
├── Debug/
│ ├── Flutter.podspec
│ ├── App.framework
│ ├── FlutterPluginRegistrant.framework
│ └── example_plugin.framework (each plugin with iOS platform code is a separate framework)
├── Profile/
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

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

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

img

img

img

img

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

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

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

【附】相关架构及资料

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

[外链图片转存中…(img-5lxyDLWt-1712764472799)]

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

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

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

【附】相关架构及资料

[外链图片转存中…(img-7APsr3WP-1712764472799)]

[外链图片转存中…(img-pLC4668L-1712764472800)]

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值