Fragment的最佳实践:一个简易版的新闻应用-填坑记录

今天照着书上把代码写了一下,本以为一样的东西绝对能跑,可以还是想简单了。 首先我是把书籍自带的源码用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,以获得更安全的类型检查。

具体替代方案

  1. 启用 View Binding 在模块的 build.gradle 中启用 View Binding:
arduino 复制代码
android {
    ...
    buildFeatures {
        viewBinding true
    }
}
  1. 通过 View Binding 获取容器 ID,结合 findFragmentById() View Binding 会为每个布局文件生成对应的绑定类(如 ActivityMainBinding),通过绑定类可以安全获取视图 ID,避免直接使用硬编码的 R.id.xxx,再结合 FragmentManagerfindFragmentById() 查找 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 之间的 "桥梁",主要负责:

  1. 管理 Activity 中添加的所有 Fragment 的生命周期(创建、销毁、切换等);
  2. 处理 Fragment 事务(如添加 add()、替换 replace()、移除 remove() 等操作);
  3. 查找已添加到 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.pngic_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.gradleapp/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.xmlpackage 属性设置应用命名空间

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.xmlpackage 属性设置应用命名空间 ,转而要求在 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.gradleapp/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 之前,应用的 "包名" 有两个来源:

  1. AndroidManifest.xmlpackage 属性:控制资源引用(如 R.id.xxx)和代码命名空间。
  2. build.gradleapplicationId:控制应用在设备上的唯一标识。

这种 "双来源" 容易导致不一致(比如改了一个没改另一个),因此新版 AGP 统一为 namespace 控制代码 / 资源命名空间,applicationId 控制设备标识 ,并废弃了 Manifest 的 package 属性,避免冲突。

常见问题排查

  1. 同步后提示 "找不到 R 类" :原因是 namespace 配置错误或未配置,导致资源生成路径异常。重新检查 build.gradlenamespace 是否与原 package 完全一致(大小写、拼写都不能错)。
  2. 组件(如 Activity)注册报错 "未声明" :无需修改 Manifest 中的组件声明(如 <activity android:name=".MainActivity"/>),namespace 会自动作为前缀,等同于原 package 的作用。
  3. 依赖库引用报错 :确保 build.gradlenamespace 与依赖库要求的包名无冲突,同步后通常会自动修复。

五、在同一个布局文件(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)
            }
        }
    }
}

关键说明

  1. 为什么废弃 adapterPositionadapterPosition 依赖于 RecyclerView 的布局状态,在数据发生变化(如调用notifyDataSetChanged())但尚未完成布局更新时,可能返回过时的位置信息,导致索引错误。

  2. bindingAdapterPosition 的优势:它直接关联到适配器的绑定状态,即使数据更新后,也能准确反映当前 item 在适配器中的实际位置,减少因位置错误导致的崩溃。

  3. 额外注意事项

    • 无论使用哪种方式,都建议判断 position != RecyclerView.NO_POSITION,避免 item 已被移除时的异常。
    • 在 Kotlin 中,可直接使用属性访问(bindingAdapterPosition),无需调用getBindingAdapterPosition()(Java 中需用 getter 方法)。
  4. RecyclerView.NO_POSITION 是什么? 它是 RecyclerView 中定义的一个常量,值为 -1,表示 "无效的位置"。当一个 item 被删除、未显示在屏幕上,或因数据变化导致位置失效时,获取到的位置会是这个值。

  5. 为什么需要这个判断? 在 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 方法

后面记录一下填坑以后的可运行版本吧

相关推荐
曹绍华1 小时前
kotlin扩展函数是如何实现的
android·开发语言·kotlin
LSL666_6 小时前
5 Repository 层接口
android·运维·elasticsearch·jenkins·repository
alexhilton10 小时前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
2501_9400940212 小时前
emu系列模拟器最新汉化版 安卓版 怀旧游戏模拟器全集附可运行游戏ROM
android·游戏·安卓·模拟器
下位子12 小时前
『OpenGL学习滤镜相机』- Day9: CameraX 基础集成
android·opengl
参宿四南河三14 小时前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我15 小时前
mmkv的 mmap 的理解
android
没有了遇见15 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong15 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强16 小时前
如何简单 hack agp 执行过程中的某个类
android