用前面介绍的限定符和 Fragment 技术实现一个新闻应用,在手机上采用单页显示目录,然后点击进入新闻内容;在平板上采用双页显示(左侧为目录,右侧为新闻内容)。
下面的示例分为以下几个部分:
创建两个 Fragment
构建 Activity 布局
MainActivity 根据布局加载对应的 Fragment 并处理目录项点击
下面我们一步步详细讲解每个部分。
这个 Fragment 用于显示新闻目录,点击目录项后通知 Activity 进行切换
package com.example.myapplication44
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.fragment.app.Fragment
//这个 Fragment 用于显示新闻目录,点击目录项后通知 Activity 进行切换
class NewsListFragment : Fragment() {
// 用于模拟新闻标题列表
private val newsTitles = listOf("新闻一", "新闻二", "新闻三", "新闻四")
/*
定义了一个可空变量 listener,类型为 OnNewsSelectedListener(这是接口类型)
这个变量用于保存 Activity 实现的回调接口的引用,以便在列表项点击时通知 Activity。
OnNewsSelectedListener 是一个接口,它用于在新闻列表中点击某一项时回调通知宿主 Activity
实现这个接口的 Activity 将在回调方法 onNewsSelected(index: Int) 中处理用户的点击事件,从而更新界面或切换到新闻详情页面。
*/
private var listener: OnNewsSelectedListener? = null
// 定义接口,用于向 Activity 通知目录项点击
interface OnNewsSelectedListener {
//在 NewsListFragment 中,当用户点击列表项时
// 会调用 onNewsSelected(position) 来通知 Activity,从而可以进行切换显示对应的新闻详情。
fun onNewsSelected(index: Int)
}
// onAttach() 方法,这个方法在 Fragment 与 Activity 关联时调用。
override fun onAttach(context: Context) {
super.onAttach(context)
//)检查宿主 Activity 是否实现了接口
if (context is OnNewsSelectedListener) {
listener = context
} else {
throw RuntimeException("$context 必须实现 OnNewsSelectedListener 接口")
}
}
//重写 onDetach() 方法,在 Fragment 与 Activity 分离时被调用
// 这里将 listener 置为 null,有助于避免内存泄露。
override fun onDetach() {
super.onDetach()
listener = null
}
/*
重写了 onCreateView() 方法,该方法负责创建并返回 Fragment 的视图层级。参数说明:
inflater:用于加载 XML 布局文件创建视图对象;
container:宿主 Activity 中包含该 Fragment 的父视图;
savedInstanceState:包含之前保存的状态数据(如有)。
*/
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// 加载列表视图布局
val view = inflater.inflate(R.layout.fragment_news_list, container, false)
val listView = view.findViewById(R.id.news_list_view)
// 使用简单的 ArrayAdapter 显示新闻标题列表
/*
创建了一个 ArrayAdapter,用于将新闻标题列表渲染到 ListView 中。
第一个参数 requireContext() 返回非空的 Context;
第二个参数指定 Android 内置的布局 resource(simple_list_item_1),用于显示每一行的文本;
第三个参数是数据源,即之前定义的 newsTitles 列表。
*/
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, newsTitles)
listView.adapter = adapter
/*
AdapterView.OnItemClickListener { _, _, position, _ -> ... }
这里使用了 lambda 表达式来实现 OnItemClickListener 接口
该接口的回调方法需要四个参数,但在我们的实现中只有第三个参数 position 是需要使用的
其余的用下划线 _ 表示不关心这些参数:
第1个参数:parent,表示包含被点击项的 AdapterView(这里是 ListView);不需要用所以用 _ 忽略。
第2个参数:view,表示被点击的具体视图;此处也没用到,因此为 _。
第3个参数:position,表示被点击项在列表中的位置索引,这里用于后续回调,通知外部调用者。
第4个参数:id,表示被点击项的行 ID,同样不使用,用 _ 忽略。
*/
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
listener?.onNewsSelected(position)
}
return view
}
}
在 res/layout 下创建文件,定义 ListView 作为新闻目录
这个 Fragment 用于显示新闻详情,通过接收新闻索引来加载内容
package com.example.myapplication44
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
//这个 Fragment 用于显示新闻详情,通过接收新闻索引来加载内容
class NewsDetailFragment : Fragment() {
// 参数key,用于传递选中的新闻索引
/*
这里定义了一个 companion object,用于存放一些与类相关但不依赖于实例的内容。
声明了一个私有常量 ARG_NEWS_INDEX,用作 Bundle 参数的 key,以便在传递数据时使用。
*/
companion object {
private const val ARG_NEWS_INDEX = "news_index"
/*
定义了一个 newInstance 的静态工厂方法,目的是创建 NewsDetailFragment 的新实例
并将所选新闻的索引封装在 Bundle 中传递给 Fragment:
创建 NewsDetailFragment 实例。
创建 Bundle 对象 args,并将整数 newsIndex 用 key ARG_NEWS_INDEX 存入。
将 Bundle 设置为 fragment 的 arguments 属性。
返回构造好的 fragment 实例。
Bundle 是 Android 中的一个数据容器类,用于存储和传递键值对形式的数据
在创建 Fragment 实例时,我们通常使用一个 Bundle 存放需要传递的参数。
通过将 Bundle 对象赋值给 fragment.arguments
Fragment 在其生命周期内可以通过 arguments 属性来访问这些数据(例如,在 onCreate 或 onCreateView 中读取传递的数据)。
这样做可以确保 Fragment 在被创建或者在系统重建(例如旋转屏幕)时,能正确保存和恢复这些参数。
工厂方法是一种创建对象的设计模式,其主要目的在于将对象的创建过程与对象的使用分离,以便隐藏创建逻辑,降低组件之间的耦合度。
通常,工厂方法会在一个专门的方法中创建和返回所需的对象,而不是在代码中直接使用构造函数
这样,如果将来需要更换创建对象的方式或对象的具体类型,只需修改工厂方法,而无需在整个代码中查找并修改对象的初始化代码。
*/
fun newInstance(newsIndex: Int): NewsDetailFragment {
val fragment = NewsDetailFragment()
val args = Bundle()
args.putInt(ARG_NEWS_INDEX, newsIndex)
fragment.arguments = args
return fragment
}
}
private var newsIndex: Int = 0
// 模拟新闻内容数据
private val newsContent = listOf(
"这是新闻一的详细内容……",
"这是新闻二的详细内容……",
"这是新闻三的详细内容……",
"这是新闻四的详细内容……"
)
/*
重写了 onCreate 方法:
调用了父类的 onCreate 方法保证 Fragment 的正常初始化。
如果 arguments 不为 null,则使用 let 作用域函数读取 Bundle 中使用 ARG_NEWS_INDEX 保存的整数值
并赋值给 newsIndex。如果在 Bundle 中没有找到该 key,则默认为 0。
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
newsIndex = it.getInt(ARG_NEWS_INDEX, 0)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// 加载详情布局
val view = inflater.inflate(R.layout.fragment_news_detail, container, false)
val textView = view.findViewById(R.id.news_detail_text)
textView.text = newsContent.getOrElse(newsIndex) { "没有对应内容" }
return view
}
}
在 res/layout 下创建文件,定义一个 TextView 显示新闻内容
在 MainActivity 中将负责加载目录和详情 Fragment。
采用不同布局文件来区分手机和平板。
文件路径:res/layout/activity_main.xml
内容只有一个 Fragment 容器
文件路径:res/layout-sw600dp/activity_main.xml
使用 LinearLayout 横向放置两个容器,一个用于新闻目录,一个用于新闻详情
MainActivity 根据当前布局结构判断是手机还是平板(双页)模式,并实现目录项点击后的跳转逻辑。
package com.example.myapplication44
import android.content.Intent
import android.os.Bundle
import android.view.View // 引入 View
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity(), NewsListFragment.OnNewsSelectedListener {
// 判断手机还是平板的方式:检查布局中是否存在 detail_container
private val isDualPane: Boolean
get() = findViewById(R.id.detail_container) != null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 根据限定符,系统会自动选择使用 res/layout/activity_main.xml 或 res/layout-sw600dp/activity_main.xml
setContentView(R.layout.activity_main)
if (isDualPane) {
// 平板模式下,在左侧加载目录,右侧默认显示第0条新闻详情
supportFragmentManager.beginTransaction()
.replace(R.id.list_container, NewsListFragment())
.commit()
supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, NewsDetailFragment.newInstance(0))
.commit()
} else {
// 手机模式下,使用 main_container,仅加载目录
supportFragmentManager.beginTransaction()
.replace(R.id.main_container, NewsListFragment())
.commit()
}
}
// 目录项点击回调,参数 index 为选中的新闻下标
override fun onNewsSelected(index: Int) {
if (isDualPane) {
// 平板模式下在右侧显示详情
supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, NewsDetailFragment.newInstance(index))
.commit()
} else {
// 手机模式下点击后启动新 Activity 展示新闻详情
val intent = Intent(this, NewsDetailActivity::class.java)
intent.putExtra("news_index", index)
startActivity(intent)
}
}
}
为了在手机模式下点击目录后跳转到详情页面,我们增加一个 NewsDetailActivity 来加载 NewsDetailFragment。
package com.example.myapplication44
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
//为了在手机模式下点击目录后跳转到详情页面,我们增加一个 NewsDetailActivity 来加载 NewsDetailFragment。
class NewsDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用一个简单的容器布局,只需要一个 FrameLayout
setContentView(R.layout.activity_news_detail)
// 获取传递的新闻索引
val newsIndex = intent.getIntExtra("news_index", 0)
// 加载详情 Fragment
/*
使用 supportFragmentManager 来管理和操作 Fragment。
beginTransaction() 开启一个 Fragment 事务
replace 方法用于替换布局中 id 为 detail_container 的 View(通常是一个 FrameLayout 容器)。
NewsDetailFragment.newInstance(newsIndex) 通过工厂方法创建 NewsDetailFragment 的实例
并将 newsIndex 传递进去,以便 Fragment 根据新闻索引显示对应的新闻详情内容。
最后,通过 commit() 方法提交这次事务,使变更立即生效。
*/
supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, NewsDetailFragment.newInstance(newsIndex))
.commit()
}
}
总体流程说明:
下面是一个简化的思维导图(使用文本的形式展示流程):
用户启动应用后,MainActivity 根据当前运行设备加载不同的布局:
在 NewsListFragment 中:
onNewsSelected(index)
通知 MainActivity。在 MainActivity 的 onNewsSelected(index)
回调:
在 NewsDetailActivity 中(仅手机模式):
NewsDetailFragment 的工作:
onCreate()
中,从 arguments 里读取传递进来的新闻索引值,并在 onCreateView()
中设置对应的新闻详情内容。┌─────────────────────────────┐
│ MainActivity │
│ (onCreate() 方法执行) │
└────────────┬────────────────┘
│
│ 根据布局判断 (isDualPane)
┌─────────┴─────────────┐
│ │
平板模式 手机模式
(有 detail_container) (只有 main_container)
│ │
▼ ▼
加载双页布局: 加载单页布局:
┌──────────────────────────┐ ┌──────────────────────┐
│ LinearLayout with two: │ │ FrameLayout with one │
│ - list_container │ │ main_container │
│ - detail_container │ └──────────────────────┘
└────────────┬─────────────┘
│
┌─────────┴─────────┐
│ │
加载 NewsListFragment 加载默认 NewsDetailFragment (初始新闻索引 0)
到 list_container 到 detail_container
│
└──► 用户点击具体新闻目录项
│
▼
NewsListFragment 调用 onNewsSelected(index)
│
▼
MainActivity 的 onNewsSelected(index) 回调
│
┌───────────┴─────────────┐
│ │
平板模式 手机模式
│ │
▼ ▼
替换 detail_container 启动 NewsDetailActivity
中的 NewsDetailFragment 并传递新闻索引 via Intent
│ │
▼ ▼
NewsDetailFragment.newInstance(newsIndex) 创建新的 Fragment
│ │
▼ ▼
新闻详细内容根据传入的新闻索引展示 NewsDetailActivity 获取 NewsIndex,
加载 NewsDetailFragment 显示详情
详细流程解释:
当 MainActivity 启动时,通过 setContentView(R.layout.activity_main)
加载布局文件。
MainActivity 调用 FragmentManager 开始事务:
在 NewsListFragment 的 onCreateView 中:
listView.onItemClickListener
调用接口方法 onNewsSelected(index)
。MainActivity 实现了 NewsListFragment 的 OnNewsSelectedListener 接口,因此收到通知后:
newInstance(newIndex)
创建 Fragment 并显示用户选中的新闻详情。在 NewsDetailActivity 中:
intent.getIntExtra("news_index", 0)
获取新闻索引。NewsDetailFragment 在其 onCreate 方法中读取传入的新闻索引,然后在 onCreateView 中根据索引在内部的模拟数据中选取对应新闻内容,并显示到 TextView 上。
这样,通过多个 Fragment 与 Activity 之间的数据传递与界面切换,就实现了根据不同设备类型展示不同界面(单页或双页)的新闻应用的整体流程。