简介:一份面向Android开发入门者与希望转向Kotlin的在职工程师的完整学习实践包,系统涵盖Kotlin基础语法、Anko框架、Android开发入门、Google官方教程实践及Java与Kotlin互操作五大模块。资源共32个文件、约1.05MB,包含xml布局与配置、kt/java源码、gradle构建脚本、png示意图及md/pdf文档,便于对照学习工程结构与代码逻辑。其中KotlinDemo-master示例工程覆盖从HelloWorld到Anko简化UI、异步任务处理的完整演示,并深入讲解Material Design、生命周期管理、数据持久化与网络通信等Google推荐实践;Java与Kotlin互操作部分则提供混合编程示例,方便在既有Java项目中渐进式迁移Kotlin。已有99人学习/下载,既适合零基础读者循序渐进掌握语法与开发流程,也适合开发者参考最佳实践提升应用质量。
1. 为什么 Kotlin 会是 Android 开发绕不过去的一关
Kotlin 入门的最大门槛不是语法,而是“学了一堆语法却不知道在项目里怎么用”。市面上很多教程把 Kotlin 当 Java 的替代品来讲,讲完空安全、数据类、协程就结束,结果读者打开 Android Studio 还是不知道怎么组织一个 Activity、怎么写布局、怎么处理按钮点击。这个标题里的项目包,正是冲着这个问题去的:先学 Kotlin 基础语法,再用 Anko 框架减少样板代码,拿 Google 官方教程练手,最后用 Java 与 Kotlin 互操作示例把两种语言在同一个工程里的协作关系讲透。适合两类人——刚学完 Java 基础语法、想转 Android 的初学者,以及在公司维护老项目、被迫在 Java 和 Kotlin 代码之间来回切换的客户端开发。学完之后你会明白,Kotlin 不是又一种“玩具语言”,而是 Google 官方钦定的 Android 开发语言,Anko 虽然热度下降,但它留下的 DSL 思路至今仍在影响 Compose 的设计。
2. Kotlin 基础语法学习:哪些语法是 Android 开发真正要用的
2.1 面向 Java 开发者的最小语法集
如果你带着 Java 基础来学 Kotlin,最应该先掌握的不是扩展函数也不是协程,而是“类怎么写”“空安全怎么处理”“when 怎么替代 switch”。Android 开发里频繁用到的 Java 抽象是接口回调、匿名内部类、静态工厂方法,对应的 Kotlin 概念是接口实现、lambda 简写、伴生对象和顶层函数。
一套适合直接照抄的最小代码骨架是这样:
// 一个典型的 Kotlin 数据模型,对应 Java 的 POJO
data class User(
val id: Long,
val name: String,
val email: String?
)
// 接口回调:Java 里要写匿名内部类,Kotlin 里可以用 lambda
interface LoginCallback {
fun onSuccess(user: User)
fun onError(code: Int, message: String)
}
fun login(username: String, callback: LoginCallback) {
// 示意逻辑,实际会走网络层
callback.onSuccess(User(1L, "test", null))
}
// 调用方式对比 Java:少了一整个 new View.OnClickListener 的样板
fun demoLogin() {
login("admin") { user ->
println("登录成功:${user.name}")
}
}
这里的重点是 data class 会自动生成 equals、hashCode、toString 和 componentN 系列方法,在写 RecyclerView 的 DiffUtil 和 Log 输出时非常省事。email 字段声明成 String?,编译器和 IDE 都会强制你在使用它的时候判空,这种“类型系统层面的空安全提醒”是 Java 完全没有的。lambda 简写看起来只是语法糖,但它在 Anko 的 DSL 里是基础中的基础,因为整个 DSL 都是靠 lambda 接收者实现的。
还有一个细节是:Kotlin 的接口方法可以有默认实现,而 Java 8 之前的接口做不到。在 Android 兼容 Java 7 的年代,Java 接口加方法意味着所有实现类都要改,Kotlin 的默认方法则没有这个负担。你在看 Anko 源码时会发现,大量接口都用 Kotlin 默认方法组织了层次结构。
2.2 Gradle 里的 Kotlin 配置:编译、依赖与常见参数
学 Kotlin 不只是学语言,还要会配置构建脚本。现在的 Android 项目里,build.gradle.kts 已经成为默认选择,但网络上的老教程很多还是 Groovy 版本,两者混在一起看就容易晕。常见做法的核心配置是这样的:
// 模块级 build.gradle(Groovy 写法)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.example.kotlinpractice"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
有四个参数值得注意:compileSdk 决定了你能用哪个版本的 Android API;minSdk 太低会导致方法数膨胀,太高又丢失用户;sourceCompatibility 和 jvmTarget 必须保持一致,否则编译出的字节码和 Kotlin 元数据不一致,会出现莫名的运行时错误。
如果你在项目里看到这样的依赖写法,它表示把 libs 目录下所有 .aar 文件全部加入编译期 classpath:
dependencies {
compileOnly fileTree(dir: 'libs', include: ['*.aar'])
}
compileOnly 表示只在编译时可见,不会打包进 APK,常用于依赖某个 SDK 但又不希望把 SDK 体积塞进安装包的情况。注意,这里 include 是文件匹配模式,如果需要同时纳入 .jar,写明 include: [' .jar', ' .aar'] 即可。实际上许多早期安卓注入类和系统级插件就是靠这种 compileOnly 方式实现的,因为它在不改变 APK 内部结构的情况下,让你能在编译期引用系统隐藏 API。
2.3 把官方教程的示例代码搬进本地工程
Google 官方教程内容很多,适合自学的包括:Activity 生命周期、RecyclerView 列表、Room 数据库、ViewModel + LiveData。它们都在 GitHub 上有示例仓库,但直接 clone 下来会有两个问题:一是依赖版本可能已经落后,二是示例仓库为了适配多设备,代码里塞满了 BuildConfig 判断,新手容易迷失。我建议的做法是新建一个空工程,把官方示例的核心类摘出来,逐文件敲一遍,然后运行。
摘录时要小心的类是:Application 子类、Activity 基类、ViewModel 工厂类。官方示例往往用到了 Hilt 依赖注入框架,Hilt 的注解处理器会生成一堆让人困惑的类。如果你还没学过 Dagger 系列,建议先把官方示例里的 Hilt 相关代码删掉,手动在 Application 里初始化一个全局的 ServiceLocator,等理解了依赖注入的价值再回来学 Hilt。这样做的理由是:你现在要学的是 Kotlin 语法和 Android 组件,不是依赖注入框架,一次引入太多概念会直接劝退。
工程里自带的 MainActivity.kt 不要全删,留着作为“冒烟测试”的入口。所谓冒烟测试就是最简单的验证:Activity 能启动、页面能显示、点击有响应。先把这三件事跑通,再谈复杂的架构。
3. Anko 框架使用:当年为什么流行,现在还能学什么
3.1 Anko 的核心思想:用 Kotlin 代码替代 XML 布局
如果你的项目标题里带 Anko 框架,那这个包大概率是 2018 到 2020 年间的产物。Anko 是 JetBrains 官方出的 Android 开发库,核心功能是用 Kotlin 的 DSL 写布局、写 Intent、写异步任务。之所以当时火,是因为 XML 布局有两个让人头大的点:findViewById 样板代码太多、跨模块引用 ID 容易出错。Anko 的 DSL 写法完全避开这两点,一行代码完成“创建控件 + 设置属性 + 添加进父容器”:
verticalLayout {
val nameEdit = editText {
hint = "请输入用户名"
textSize = 16f
}
button("登录") {
onClick {
doLogin(nameEdit.text.toString())
}
}.lparams(width = matchParent, height = wrapContent)
}
这是一个标准的 Anko DSL 代码块。verticalLayout 是 Anko 对 LinearLayout 的扩展函数,在这个 lambda 接收者里,editText、button 都是直接创建控件的扩展函数。setOnClickListener 被简写成 onClick,lparams 用来设置 LayoutParams,对应 XML 里的 layout_width 和 layout_height。
逻辑说明:lambda 接收者让“创建控件”和“设置属性”在同一个作用域里完成,你不需要持有控件的引用就能设置它的属性。参数说明:verticalLayout 里默认的 orientation 是 vertical,如果你要水平排列,写 horizontalLayout;lparams 里的 matchParent 和 wrapContent 是 Anko 定义的结构化常量,不是魔法字符串。
值得说的是,Anko 看起来已经被官方废弃,原因是 Compose 出现了。但 Compose 的很多设计思想和 Anko 一脉相承——用代码描述 UI、组合优于继承、响应式状态管理。你现在学 Anko,不是为了在生产环境用它,而是为了理解“Kotlin DSL 到底是怎么设计出来的”,这个理解可以平滑迁移到 Compose 的学习中。
3.2 Anko 中 Intent 与 Spinner 事件的关键写法
Anko 的 Intent 扩展库也很实用,它把显式跳转的写法压缩到一行:
startActivity<DetailActivity>(
"userId" to userId,
"fromMain" to true
)
在 Anko 中,startActivity 是 Context 的扩展函数,泛型参数是目标 Activity 的类名,后面的可变参数对是要传递的 Intent Extra。接收方用 intent.getStringExtra("userId") 获取,和 Java 原写法没有区别。
Android 开发里适配器下拉框(也就是 Spinner)的监听,在传统 Java 里要写 AdapterView.OnItemSelectedListener 接口的四个方法,Kotlin + Anko 时代可以用标准函数简写,不过实际生产里我用得最多的是普通 Kotlin 版本的写法,因为 Anko 的监听器扩展在最新 AndroidX 里有些兼容问题:
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
// position 就是当前选中的索引,对应数据源 List 的下标
val selected = dataList[position]
updateUi(selected)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// 没有选中项时保持默认状态
}
}
注意 onItemSelected 里的 view 参数可能为 null,因为 Spinner 在收起状态时,选中项视图不一定附着在窗口上。这是我的血泪经验:不要在 onItemSelected 里直接对 view 做动画,新手最容易在这里崩溃。
3.3 Anko 协程:比 AsyncTask 更轻的异步方案
Anko 自带了一套协程扩展,最典型的是 doAsync 和 uiThread:
doAsync {
val userList = api.fetchUserList()
uiThread {
recyclerView.adapter?.let { adapter ->
(adapter as UserAdapter)?.updateData(userList)
}
}
}
doAsync 在后台线程池执行,uiThread 把结果抛回主线程更新 UI。这个模式解决的痛点是:Java 时代你在 onPostExecute 里更新 UI 还经常担心 Activity 销毁后空指针,而 uiThread 会帮你判断当前是否有可用的 UI 线程。
不过现在协程已经进了 Kotlin 标准库,google 官方推荐的是 lifecycleScope.launch + Dispatchers.IO。Anko 的协程抽的层太薄,出错时堆栈信息不友好。学它的价值在于了解协程最简单原始的使用姿势,后面学 lifecycleScope 时对比着看会理解更快的。
4. Android 开发入门实践:从官方教程到能跑的项目
4.1 移植 Android Studio 项目的正确顺序
如果你拿到了一个旧项目想移植到新版 Android Studio,或者想把官方教程工程导入自己的电脑,最常见的问题就是 Gradle 版本不匹配导致的翻车。先打开 andriod 项目根目录下的 gradle-wrapper.properties 文件,看 distributionUrl。如果你的 Android Studio 自带 Gradle 是 8.5,而项目指定的是 6.7,大概率会直接提示无法解析依赖日志。
合理移植顺序是:先把 Gradle 版本调到和本地 Android Studio 接近,再升 AGP 版本,最后处理依赖版本冲突。不要反过来先改依赖再改 Gradle,否则报错信息是“某某方法找不到”,看起来像代码问题,实际是构建链路的版本问题,新人会在这个坑里绕好几个小时。修改的时候用 Android Studio 的迁移助手是常见做法,它在 Tools > AGP Upgrade Assistant 里,会逐步提示你改哪些文件。
环境变量配置也是新人重灾区。JDK 的 JAVA_HOME 在编译和运行 Gradle 时都会用到。如果你装了多个 JDK,Android Studio 默认用的是自己捆绑的 JBR(JetBrains Runtime),命令行编译的时候才读 JAVA_HOME。两个环境里的 JDK 版本不一致时,会出现“Unsupported class file major version”的报错,处理办法是让两边都用同一个 JDK 17。
4.2 用 Kotlin 写一个带文件读取与权限申请的页面
热词里频繁出现 content:// 开头的字符串,那是 Android 的 ContentProvider 协议,用来跨应用访问文件。以我写过的“读取微信分享文件”类需求为例,Kotlin 的典型实现会涉及到 FileProvider 的 URI 解析。注意,这里我们是在做本地的 Android 开发实践教程,不会涉及任何网络绕过类工具,只谈应用间文件分享的标准做法。
// 在 Activity 或 Fragment 中接收外部应用分享的文件
class ShareReceiverActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) {
intent.data?.let { uri ->
// 读取 ContentResolver 提供的输入流
val inputStream = contentResolver.openInputStream(uri)
val content = inputStream?.bufferedReader()?.use { it.readText() }
showContent(content)
}
}
}
}
这里的关键点是:你从分享协议里拿到的 uri,不一定是 file:// 开头的路径,很多情况下是 content:// 开头。比如微信分享来的文件就是 “content://com.tencent.wework.fileprovider/external_path/...”,QQ 的则是 “content://com.tencent.mobileqq.sharefileprovider/external_files/...”,浏览器下载的文件是 “content://com.baidu.searchbox.fileprovider/baiddpath/...”。直接用 File(uri.toString()) 去读必挂,必须经过 contentResolver.openInputStream。
另一个相关坑是:如果这个 Activity 在 AndroidManifest.xml 里声明了 android:exported="true",那么从外部应用进来时,必须自己管理读写权限。Android 11 以后,如果需要读其他应用的 content URI,往往需要在 intent 里加 Intent.FLAG_GRANT_READ_URI_PERMISSION,否则即使你的 App 有存储权限也读不到。看到 SecurityException 时八成是这个问题。
4.3 常见 UI 组件在 Kotlin 中的自定义写法
Android 开发入门的核心是几个组件:RecyclerView、ViewPager2、CoordinatorLayout。Kotlin 里写 RecyclerView 的 Adapter,最朴素但不踩坑的写法是:
class ItemAdapter(
private val items: List<ItemModel>,
private val onClick: (ItemModel) -> Unit
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {
inner class ItemViewHolder(val binding: ItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val binding = ItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ItemViewHolder(binding)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
holder.binding.nameText.text = item.name
holder.binding.root.setOnClickListener {
onClick(item)
}
}
override fun getItemCount(): Int = items.size
}
onClick 作为构造参数传入,让 Adapter 对外不依赖任何 View 层细节,这也是 Kotlin 函数类型作为接口替代品的典型案例。inner class 修饰的 ViewHolder 能访问外部类的上下文,如果你写成普通 class,bind 的时候会报无法引用外部类的错误。
如果用 ViewBinding,注意 buildFeatures { viewBinding true } 要在模块级 build.gradle 里开启。老工程如果之前没开,新加代码后在 AS 里找不到生成的 Binding 类是正常现象,先 Sync 一下,让 IDE 生成代码,再回来引用。
4.4 中文环境与工具链的琐碎配置
现在 Android Studio 的新版本其实不用额外设置中文,因为从 2024 年起,JetBrains 官方已经在 IDE 内置了中文语言包。网络上搜“android studio怎么设置中文?”,大部分教程教你去 Plugins 里搜索 Chinese Language Pack,这本身没有问题,但我更建议把它当作一个翻译环境而不是依赖项。真正需要配置的是 Gradle 的国内镜像,因为部分官方源可能访问不稳,把仓库替换成阿里云的公共仓库或腾讯云镜像能显著提升依赖下载速度。
替换位置是项目根目录 settings.gradle 里的 pluginManagement 和 dependencyResolutionManagement 两个闭包,把 mavenCentral() 和 google() 替换成镜像地址,但注意保留 google() 在第一个位置,因为很多 AndroidX 库只在 Google 仓库发布,镜像不一定同步完整。
5. Java 与 Kotlin 互操作示例:5 个必须知道的坑
5.1 空安全类型在边界处的失效
Kotlin 声称空安全是语言级能力,但 Java 没有这个限制。Java 代码返回给 Kotlin 的值被定义为“平台类型”,编译器允许你用,也允许你在运行时收到 null。典型的翻车场景是服务器返回的 JSON 反序列化成 Java Bean 再传进 Kotlin 层。现象:Kotlin 层写了个非空参数,运行时却拿到了 null,报 KotlinNullPointerException。原因:Gson 或 Jackson 调用 Java 类的默认构造器创建对象,字段未赋值就是 null,Kotlin 的非空检查管不到 Java 侧的行为。解决:在互操作边界处显式判空。
// Java 传入的 JavaUser 可能带 null 字段
fun handleJavaUser(javaUser: JavaUser?) {
val name = javaUser?.name ?: "unknown"
// 不要直接写 javaUser.name,平台类型不保证非空
saveName(name)
}
这里 ?. 安全调用和 ?: 默认值兜底是 Kotlin 互操作最基本的防线。参数说明:Elvis 操作符 ?: 左侧为 null 时执行右侧表达式,常用于为空时给默认值,比 if-else 少三行代码。如果你的项目里 Java 代码是历史遗留的老代码,建议把互操作函数统一放到一个 ext 文件里,把判空逻辑集中管理,这样后面 Kotlin 化推进时能减少重复劳动。
5.2 顶层函数和伴生对象在 Java 里叫什么名字
Kotlin 里你写了一个顶层函数:
// FileName: StringUtils.kt
fun isBlank(input: String?): Boolean {
return input.isNullOrBlank()
}
Java 侧调用它要写 StringUtilsKt.isBlank(str),Kotlin 编译器把顶层函数编译成名为 FileNameKt 的类里的静态方法。如果你忘了类名后缀 Kt 就会报错。同理,伴生对象里的函数在 Java 侧是通过 Companion 访问的:
// Kotlin 侧
object ApiClient {
fun create(): ApiClient = ApiClient()
}
// Java 侧
ApiClient.Companion.create();
如果你想让 Java 侧调用更自然,加上 @JvmStatic 注解,Java 侧就可以写成 ApiClient.create(),和 Kotlin 侧语法一模一样。互操作示例在遇到性能敏感的老代码改造时特别常见,比如把一个 Java 工具类改成 Kotlin object,然后 Java 代码继续用原名调用,这里必须补 @JvmStatic,否则同事编译不过会直接骂你。
5.3 默认参数与 Java 重载不兼容
Kotlin 允许函数有默认参数,Java 不知道默认参数这个概念,所以不会自动生成两个重载方法。如果你 Kotlin 侧写了:
fun loadData(id: String, retry: Int = 3)
Java 侧调用 loadData("123") 会直接编译失败,因为在 Java 视角里这是只有一个参数的函数,并没有双参版本。解决:加 @JvmOverloads 注解,编译器会生成冗余的重载方法。注意,@JvmOverloads 只能在一个方法最多有 32 个参数的限制下工作,超过会报错。或者干脆在 Java 侧老老实实传全参数,写法只是难看一点,但行为没有歧义。
5.4 sealed class 与 Java 的 enum 坑
sealed class 在 Kotlin 互操作示例里经常用来表达受限的 UI 状态:
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
这个写法在 Kotlin 侧用 when 穷尽匹配很优雅,但 Java 侧调用 UiState 需要逐个导入子类。常见做法是保持 sealed class 但尽量避免 Java 侧直接频繁分支,因为 Java 的 switch 语法对 sealed class 没有 Kotlin 的穷尽检查,漏掉分支编译期不会报错,运行期直接进入 default。这是新手最容易忽略的互操作边界:Kotlin 的编译器帮你做检查,Java 编译器不做。
5.5 Anko 与 Java 8 时代的兼容问题
如果你从标题里的项目包翻到 Anko 的实际使用代码,大概率会发现它引用了 support-v4 库,与现在默认的 AndroidX 冲突。现象:编译报 duplicate class,或者在 Manifest 合并时出现 conflict。原因:Anko 最后稳定版是 0.10.8,发布于 2019 年,那时候 AndroidX 迁移刚完成,很多示例是基于 support 库写的。解决:要么把 Anko 依赖全部移除,只阅读逻辑;要么用 jetified 方式强制重映射到 AndroidX。具体操作是:
dependencies {
implementation "org.jetbrains.anko:anko-commons:0.10.8"
implementation "org.jetbrains.anko:anko-sdk15:0.10.8"
}
然后在 gradle.properties 里加 android.enableJetifier=true,让 Android 构建工具自动把 support 库替换为 AndroidX 等价类。注意 Jetifier 会影响构建耗时,而且并非所有 support 类的替换都完美,遇到资源 ID 找不到的时候,查一下是不是 Jetifier 映射没覆盖到。更推荐的做法是,把 Anko 当作历史资料阅读,自己动手实现相同功能时直接用 ViewBinding 或 Compose。
6. 把回调改造成 suspend:一个够用到工作里的协程技巧
我们在真实项目里维护旧蓝牙代码时,最痛苦的是 BluetoothGattCallback 的回调地狱。Android 的蓝牙 API 设计得很老派,每次连接状态变化、每次读 RSSI 都通过回调通知,你把这些回调包在 suspend 函数里,能把十几个嵌套回调压成顺序代码。这个技巧建议你单独开个小工程验证,也顺便检验你对 Kotlin 协程的理解程度。
suspend fun Context.waitForConnection(gatt: BluetoothGatt): Boolean =
suspendCancellableCoroutine { continuation ->
val callback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt?,
status: Int,
newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
continuation.resume(true)
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
continuation.resume(false)
}
}
}
gatt.registerCallback(callback)
continuation.invokeOnCancellation {
gatt.unregisterCallback(callback)
}
}
这段代码的关键是 suspendCancellableCoroutine,它挂起当前协程并返回一个 Continuation,在回调触发时手动恢复。参数说明:Continuation 只能被 resume 一次,回调重复触发要加状态判断,否则第二次 resume 会抛 IllegalStateException。invokeOnCancellation 是协程取消时的清理钩子,把回调注销掉,避免内存泄漏。
改造步骤建议从小处入手。先找一个你项目里最简单的回调接口,比如网络请求的 success/failure,改成 suspend 版本;再练一个系统级回调,比如权限申请的 onRequestPermissionsResult;最后再碰蓝牙这种重复回调的场景。验证方法很简单:写一个测试按钮,连续反复触发连接和断开,看协程能否每次都正确恢复,以及 Activity 销毁时调用的取消能否真正注销回调。这是我验证协程代码最常用的一套动作,实测能发现不少“看起来没问题、一断开就泄漏”的隐患。开发这几年我最大的体会是:Kotlin 的协程和互操作并不是语法问题,而是边界问题——边界处的判空、Java 侧的调用方式、回调的恢复时机,才是项目里真正掉过头发的地方。希望这份基于实战视角的拆解帮你在 Kotlin 这条路上少踩几个坑,把项目真正跑起来。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_29195217/article/details/169838864



