今天照着书上把代码写了一下,本以为一样的东西绝对能跑,可以还是想简单了。 首先我是把书籍自带的源码用android studio打开,想先跑一下。 结果发现各种报错,应该是studio版本和代码中的不匹配,代码有点老了。
然后用studio重新创建个项目。把app/src/main下的代码都复制到新项目中。 结果发现还是各种报错。 这里记录一下过程中的问题,以防忘记。
一、 首先是kotlin-android-extensions 过期的问题
kotlin-android-extensions 过期以后,findFragmentById()用什么替代?
在 kotlin-android-extensions
被废弃后,官方推荐使用 View Binding 作为替代方案来安全地访问视图资源,包括 Fragment 的查找。对于 findFragmentById()
本身,它本质上是 FragmentManager
的 API,并非 kotlin-android-extensions
提供的功能,因此仍可继续使用,但建议结合 View Binding 来获取布局中的 Fragment 容器 ID,以获得更安全的类型检查。
具体替代方案
- 启用 View Binding 在模块的
build.gradle
中启用 View Binding:
arduino
android {
...
buildFeatures {
viewBinding true
}
}
- 通过 View Binding 获取容器 ID,结合
findFragmentById()
View Binding 会为每个布局文件生成对应的绑定类(如ActivityMainBinding
),通过绑定类可以安全获取视图 ID,避免直接使用硬编码的R.id.xxx
,再结合FragmentManager
的findFragmentById()
查找 Fragment。
示例代码
1. 在 Activity 中查找 Fragment
假设布局文件 activity_main.xml
中有一个 Fragment 容器:
xml
<!-- activity_main.xml -->
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
通过 View Binding 查找 Fragment:
kotlin
import com.example.yourpackage.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
// 声明ViewBinding变量
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化ViewBinding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 通过ViewBinding获取容器ID,结合findFragmentById()查找Fragment
val myFragment = supportFragmentManager.findFragmentById(binding.fragmentContainer.id) as? MyFragment
// 如果Fragment未实例化,可进行添加
if (myFragment == null) {
supportFragmentManager.beginTransaction()
.add(binding.fragmentContainer.id, MyFragment())
.commit()
}
}
}
这里supportFragmentManager
是 AndroidX 库中 FragmentActivity
(及其子类,如 AppCompatActivity
)提供的一个属性,用于获取管理当前 Activity 中所有 Fragment 的 FragmentManager
实例
核心作用:
它是 Activity 与 Fragment 之间的 "桥梁",主要负责:
- 管理 Activity 中添加的所有 Fragment 的生命周期(创建、销毁、切换等);
- 处理 Fragment 事务(如添加
add()
、替换replace()
、移除remove()
等操作); - 查找已添加到 Activity 中的 Fragment(通过
findFragmentById()
或findFragmentByTag()
)。
为什么叫 "support"?
这个命名源于早期的 Android 支持库(Support Library)。在 Android 系统早期,原生系统对 Fragment 的支持不完善,Google 推出了支持库来兼容低版本系统。后来支持库升级为 AndroidX (现在的官方推荐库),supportFragmentManager
也随之成为 AndroidX 中管理 Fragment 的标准接口。
与其他 FragmentManager 的区别:
supportFragmentManager
:属于 Activity,用于管理 Activity 直接包含的 Fragment。childFragmentManager
:属于 Fragment,用于管理当前 Fragment 内部包含的 "子 Fragment"(Fragment 嵌套场景)。
2. 在 Fragment 中查找子 Fragment
如果需要在一个 Fragment 中查找其内部的子 Fragment,应使用 childFragmentManager
:
kotlin
class ParentFragment : Fragment() {
private lateinit var binding: FragmentParentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentParentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 查找子Fragment(使用childFragmentManager)
val childFragment = childFragmentManager.findFragmentById(binding.childFragmentContainer.id) as? ChildFragment
if (childFragment == null) {
childFragmentManager.beginTransaction()
.add(binding.childFragmentContainer.id, ChildFragment())
.commit()
}
}
}
优势说明
- 类型安全:View Binding 生成的绑定类会对视图 ID 进行类型检查,避免因 ID 错误导致的运行时异常。
- 空安全:绑定类会自动处理视图的空值情况,无需手动判空。
- 替代 synthetic 扩展 :完全替代了
kotlin-android-extensions
的 synthetic 视图访问方式,是官方推荐的长期解决方案
二、newsContentFrag as NewsContentFragment 报错Unresolved reference: newsContentFrag
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_news_content)
val title = intent.getStringExtra("news_title") // 获取传入的新闻标题
val content = intent.getStringExtra("news_content") // 获取传入的新闻内容
if (title != null && content != null) {
val fragment = newsContentFrag as NewsContentFragment
fragment.refresh(title, content) //刷新NewsContentFragment界面
}
}
这里直接写 val fragment = newsContentFrag as NewsContentFragment 是kotlin-android-extensions的写法。这里改成
ini
val fragment = supportFragmentManager
.findFragmentById(R.id.news_content_frag) as NewsContentFragment
然后还把activity_news_content.xml中fragment的id从NewsContentFrag改成news_content_frag
三、同名但不同格式的资源文件导致的报错
swift
报错AGPBI: {"kind":"error","text":"Duplicate resources","sources":[{"file":{"description":"mipmap-hdpi-v4/ic_launcher","path":"D:\\androidhome\\FragmentDemo\\app\\src\\main\\res\\mipmap-hdpi\\ic_launcher.png"}},{"file":{"description":"mipmap-hdpi-v4/ic_launcher","path":"D:\\androidhome\\FragmentDemo\\app\\src\\main\\res\\mipmap-hdpi\\ic_launcher.webp"}}],"tool":"Resource and asset merger"}
这个错误的原因是:在同一个资源目录(mipmap-hdpi
)下存在同名但不同格式的资源文件 (ic_launcher.png
和 ic_launcher.webp
),导致 Android 资源合并工具无法区分,从而报 "重复资源" 错误。
解决步骤:
定位重复文件 打开项目目录 D:\androidhome\FragmentDemo\app\src\main\res\mipmap-hdpi
,会看到两个文件:
markdown
- `ic_launcher.png`
- `ic_launcher.webp`
然后同名的删掉一个就行,我删了ic_launcher.webp
。 其他文件找了一下也把同名的删掉了
四、依赖库版本与编译 SDK 版本不兼容
vbnet
报错 xecution failed for task ':app:checkDebugAarMetadata'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.CheckAarMetadataWorkAction
> An issue was found when checking AAR metadata:
1. Dependency 'androidx.recyclerview:recyclerview:1.4.0' requires libraries and applications that
depend on it to compile against version 35 or later of the
Android APIs.
:app is currently compiled against android-34.
Also, the maximum recommended compile SDK version for Android Gradle
plugin 8.5.2 is 34.
这个错误的核心原因是 依赖库版本与编译 SDK 版本不兼容 :androidx.recyclerview:recyclerview:1.4.0
要求项目编译 SDK 版本至少为 35 ,但你的项目当前使用的是 34,且当前 Android Gradle 插件(AGP 8.5.2)最高推荐的编译 SDK 版本也是 34,导致冲突。
解决方案(二选一)
方案 1:降低 RecyclerView 版本以适配当前编译 SDK 34(推荐,改动小)
recyclerview:1.4.0
是较新版本,对编译 SDK 要求较高。可以降级到兼容 SDK 34 的版本(如 1.3.2
),无需修改编译 SDK 版本。
修改 模块级 build.gradle
(app/build.gradle
)中的依赖:
arduino
dependencies {
// 将原来的 1.4.0 改为 1.3.2
implementation 'androidx.recyclerview:recyclerview:1.3.2'
// 其他依赖...
}
我实际改的是gradle/libs.versions.toml 中的
ini
recyclerview = "1.3.2"
这个版本是这样找的
build.gradle.kts中有
scss
dependencies {
implementation(libs.androidx.recyclerview)
}
这个对应 libs.versions.toml中的
csharp
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
这里version.ref = "recyclerview" }就是对应recyclerview = "1.3.2"
四、新版 Android Gradle 插件(AGP 7.0+)已废弃通过 AndroidManifest.xml
的 package
属性设置应用命名空间
ini
Incorrect package="com.example.fragmentbestpractice" found in source AndroidManifest.xml: D:\androidhome\FragmentDemo\app\src\main\AndroidManifest.xml.
Setting the namespace via the package attribute in the source AndroidManifest.xml is no longer supported.
Recommendation: remove package="com.example.fragmentbestpractice" from the source AndroidManifest.xml: D:\androidhome\FragmentDemo\app\src\main\AndroidManifest.xml.
报错
这个错误的核心原因是 新版 Android Gradle 插件(AGP 7.0+)已废弃通过 AndroidManifest.xml
的 package
属性设置应用命名空间 ,转而要求在 build.gradle
中统一配置 namespace
。Manifest 中保留的 package
属性与 Gradle 配置冲突,导致编译失败。
解决
步骤 1:删除 AndroidManifest.xml 中的 package
属性
打开报错的 Manifest 文件(路径:D:\androidhome\FragmentDemo\app\src\main\AndroidManifest.xml
),找到根标签 <manifest>
中的 package
属性并删除:
xml
<!-- 修改前:<manifest> 标签包含 package 属性(错误根源) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.fragmentbestpractice"> <!-- 删掉这行的 package 属性 -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
...>
<!-- 组件声明(Activity/Fragment等)不变 -->
</application>
</manifest>
<!-- 修改后:<manifest> 标签无 package 属性 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 其余内容完全不变 -->
<application ...>...</application>
</manifest>
步骤 2:确认 build.gradle 中已配置 namespace
(关键,否则会丢失包名)
新版 AGP 要求在 模块级 build.gradle
(app/build.gradle
)的 android
闭包中通过 namespace
指定应用包名,且值必须与之前 Manifest 中的 package
一致(即 com.example.fragmentbestpractice
)。
检查并补充配置:
arduino
android {
namespace "com.example.fragmentbestpractice" // 必须添加这行,值与原package一致
compileSdk 34 // 你的编译SDK版本(如34/35)
defaultConfig {
applicationId "com.example.fragmentbestpractice" // (可选但建议保留)与namespace一致
minSdk 21 // 你的最小SDK版本
targetSdk 34 // 你的目标SDK版本
...
}
...
}
原理说明:为什么 Manifest 的 package 属性被废弃?
在 AGP 7.0 之前,应用的 "包名" 有两个来源:
AndroidManifest.xml
的package
属性:控制资源引用(如R.id.xxx
)和代码命名空间。build.gradle
的applicationId
:控制应用在设备上的唯一标识。
这种 "双来源" 容易导致不一致(比如改了一个没改另一个),因此新版 AGP 统一为 namespace
控制代码 / 资源命名空间,applicationId
控制设备标识 ,并废弃了 Manifest 的 package
属性,避免冲突。
常见问题排查
- 同步后提示 "找不到 R 类" :原因是
namespace
配置错误或未配置,导致资源生成路径异常。重新检查build.gradle
的namespace
是否与原package
完全一致(大小写、拼写都不能错)。 - 组件(如 Activity)注册报错 "未声明" :无需修改 Manifest 中的组件声明(如
<activity android:name=".MainActivity"/>
),namespace
会自动作为前缀,等同于原package
的作用。 - 依赖库引用报错 :确保
build.gradle
的namespace
与依赖库要求的包名无冲突,同步后通常会自动修复。
五、在同一个布局文件(activity_main.xml
)的不同变体(如不同屏幕尺寸、横竖屏等配置)中,根元素的 id
不一致
rust
Execution failed for task ':app:dataBindingGenBaseClassesDebug'.
> Configurations for activity_main.xml must agree on the root element's ID.
这个错误的核心原因是:在同一个布局文件(activity_main.xml
)的不同变体(如不同屏幕尺寸、横竖屏等配置)中,根元素的 id
不一致,导致 Data Binding 生成基类时无法统一处理。
具体分析
Android 允许为不同场景(如横竖屏、不同屏幕尺寸)创建布局变体(例如:
res/layout/activity_main.xml
(默认)res/layout-sw600dp/activity_main.xml
(平板)
这些布局文件虽然文件名相同,但如果它们的根元素 id
不同 (例如一个根元素是 android:id="@+id/main_root"
,另一个是 android:id="@+id/root_layout"
),Data Binding 就会报错,因为它需要统一的根元素 ID 来生成绑定类。
这里其实是因为res/layout-sw600dp/activity_main.xml
的linearLayout标签有id
ini
android:id="@+id/news_title_layout"
res/layout/activity_main.xml
里没有
六、'onActivityCreated(Bundle?): Unit' is deprecated. Deprecated in Java
onActivityCreated()
是 Fragment 中的一个生命周期方法,主要用于在 Activity 的 onCreate()
完成后执行初始化操作。但随着 Android 开发规范的演进,这个方法因可能导致 Fragment 与 Activity 过度耦合而被 废弃(deprecated) 。
替代方案:使用 onViewCreated()
官方推荐将原本在 onActivityCreated()
中的逻辑迁移到 onViewCreated()
中。onViewCreated()
在 Fragment 的视图(View
)创建完成后立即调用,此时可以安全地访问视图控件,且无需依赖 Activity 的生命周期状态。
代码迁移示例
旧代码(使用 onActivityCreated()
,已废弃):
kotlin
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// 初始化视图(如设置RecyclerView适配器、绑定数据等)
val recyclerView = view?.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView?.adapter = MyAdapter(dataList)
}
新代码(使用 onViewCreated()
,推荐):
kotlin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接使用参数中的 view 访问控件,或通过 ViewBinding 获取
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = MyAdapter(dataList)
// 如果使用 ViewBinding:
// binding.recyclerView.adapter = MyAdapter(dataList)
}
七、'getter for adapterPosition: Int' is deprecated. Deprecated in Java
在 RecyclerView 的 ViewHolder 中,adapterPosition
属性已被废弃,官方推荐使用 bindingAdapterPosition
替代。这一变更的核心原因是adapterPosition
在某些场景(如数据动态更新时)可能返回不准确的位置,而bindingAdapterPosition
能更可靠地反映当前 item 在绑定适配器中的位置。
新代码(使用 bindingAdapterPosition
,推荐):
scss
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
// 替换为 bindingAdapterPosition
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
// 处理点击逻辑(位置更可靠)
onItemClick(position)
}
}
}
}
关键说明
-
为什么废弃
adapterPosition
?adapterPosition
依赖于 RecyclerView 的布局状态,在数据发生变化(如调用notifyDataSetChanged()
)但尚未完成布局更新时,可能返回过时的位置信息,导致索引错误。 -
bindingAdapterPosition
的优势:它直接关联到适配器的绑定状态,即使数据更新后,也能准确反映当前 item 在适配器中的实际位置,减少因位置错误导致的崩溃。 -
额外注意事项:
- 无论使用哪种方式,都建议判断
position != RecyclerView.NO_POSITION
,避免 item 已被移除时的异常。 - 在 Kotlin 中,可直接使用属性访问(
bindingAdapterPosition
),无需调用getBindingAdapterPosition()
(Java 中需用 getter 方法)。
- 无论使用哪种方式,都建议判断
-
RecyclerView.NO_POSITION
是什么? 它是 RecyclerView 中定义的一个常量,值为-1
,表示 "无效的位置"。当一个 item 被删除、未显示在屏幕上,或因数据变化导致位置失效时,获取到的位置会是这个值。 -
为什么需要这个判断? 在 RecyclerView 中,item 的位置可能因为以下情况变得无效:
- 该 item 已经被从数据源中删除(但尚未从屏幕上移除);
- 数据发生了变化(如调用了
notifyDataSetChanged()
),但布局还未完成更新; - 用户快速点击时,item 可能已经被回收或移除。
此时如果直接使用
position
(可能为-1
)去操作数据源(如获取数据、更新 UI 等),会导致类似 "数组索引越界" 的错误。
八、NewsTitleFragment 中使用private lateinit var activityMainBinding: ActivityMainBinding 的报错
kotlin
报错at com.example.fragmentdemo.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:53)
at com.example.fragmentdemo.NewsTitleFragment.onViewCreated(NewsTitleFragment.kt:31)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128) 代码是:class NewsTitleFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接使用参数中的 view 访问控件,或通过 ViewBinding 获取
isTwoPane = activity?.findViewById<View>(R.id.news_content_layout) != null
val layoutManager = LinearLayoutManager(activity)
newsTitleFragBinding = NewsTitleFragBinding.inflate(layoutInflater)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
val newsTitleRecyclerView = newsTitleFragBinding.newsTitleRecyclerView
newsTitleRecyclerView.layoutManager = layoutManager
val adapter = NewsAdapter(getNews())
newsTitleRecyclerView.adapter = adapter
}
问题出在 Fragment 中不恰当地初始化了 Activity 的 ViewBinding。在 Fragment 中直接创建 ActivityMainBinding 实例是不合适的,这违背了组件的职责划分。
正确的做法是 Fragment 只负责管理自己的视图绑定,而不是直接操作 Activity 的绑定。
修改后:
kotlin
package com.example.fragmentdemo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.fragmentdemo.databinding.NewsTitleFragBinding
import java.util.Random
class NewsTitleFragment : Fragment() {
private lateinit var newsTitleFragBinding: NewsTitleFragBinding
// private lateinit var activityMainBinding: ActivityMainBinding
private var isTwoPane = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.news_title_frag, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接使用参数中的 view 访问控件,或通过 ViewBinding 获取
isTwoPane = activity?.findViewById<View>(R.id.news_content_layout) != null
val layoutManager = LinearLayoutManager(activity)
newsTitleFragBinding = NewsTitleFragBinding.bind(view)
// activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
val newsTitleRecyclerView = newsTitleFragBinding.newsTitleRecyclerView
newsTitleRecyclerView.layoutManager = layoutManager
val adapter = NewsAdapter(getNews())
newsTitleRecyclerView.adapter = adapter
}
private fun getNews(): List<News> {
val newsList = ArrayList<News>()
for (i in 1..50) {
val news =
News("This is news title $i", getRandomLengthString("This is news content $i. "))
newsList.add(news)
}
return newsList
}
private fun getRandomLengthString(str: String): String {
val n = Random().nextInt(20) + 1
return str * n
}
inner class NewsAdapter(val newsList: List<News>) :
RecyclerView.Adapter<NewsAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val newsTitle: TextView = view.findViewById(R.id.newsTitle)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.news_item, parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val news = newsList[holder.bindingAdapterPosition]
if (isTwoPane) {
// 如果是双页模式,则刷新NewsContentFragment中的内容
// val fragment = activityMainBinding.newsContentLayout as NewsContentFragment
// fragment.refresh(news.title, news.content) //刷新NewsContentFragment界面
// 双页模式:更新右侧 Fragment(通过 FragmentManager 替换)
val fragment = NewsContentFragment.newInstance(news.title, news.content)
parentFragmentManager.beginTransaction()
.replace(R.id.news_content_layout, fragment) // 这里用 ID 而非 Binding
.commit()
} else {
// 如果是单页模式,则直接启动NewsContentActivity
NewsContentActivity.actionStart(parent.context, news.title, news.content);
}
}
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val news = newsList[position]
holder.newsTitle.text = news.title
}
override fun getItemCount() = newsList.size
}
}
注意这里在 NewsContentFragment
中添加 newInstance
方法
后面记录一下填坑以后的可运行版本吧