一、Android MVP项目下的新闻App
准备引入的三方库:
//retrofit2
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation "com.github.bumptech.glide:glide:4.9.0" //加载图片
implementation ("com.squareup.okhttp3:okhttp:4.10.0")
先看看MainActivity加载的布局
接下来,我们再看看ItemTabBinding即使Tab布局代码
1.先看看MainActivity代码:
class MainActivity : BaseMVPActivity<ActivityMainBinding, List<TabNewsData>, MainPresenter>() {
override fun initData(savedInstanceState: Bundle?) {
LoadingDialogManager.loadingDialog(this, "加载数据中") //启动一个弹出提示用户正在获取数据
presenter.startGetData() //通过P层,获取数据
}
override fun dataCall(state: Int, message: String, dataList: List<TabNewsData>?) {
LoadingDialogManager.loadOff() // 这是获取到数据。第一件事是关闭弹窗
if (state == 200 && !dataList.isNullOrEmpty()) {
setNewsTab(dataList) //200即返回的List不为null 直接下一步操作
} else {
message.showToast() //否则看看返回的信息消息是什么。
}
}
//这里是实现导航栏效果
private fun setNewsTab(dataList: List<TabNewsData>) {
val fragments = mutableListOf<Fragment>() //准备与之对应的Fragment
dataList.forEach { //循环List,把每一Item放在对应的NewsFragment中
fragments.add(NewsFragment().apply {
// 这里 初始化数据传递给Fragment使用setArguments(Bundle args)方法
// 不可以使用构造方法传递参数,使用构造方法传递参数,数据会丢失
// MyBundleUtils,一个封装类,Bundle 数据的封装
arguments = MyBundleUtils.setKeyData(it)
})
}
//这里是我封装的 tabLayout 和ViewPage2的一个封装类,
NavigationBarHManager<ItemTabBinding, TabNewsData>(binding.tabLayout, binding.viewPager2)
.setDataSources(fragments, dataList)
.setOffscreenPageLimit(-1)
.setTabMode(TabLayout.MODE_SCROLLABLE)
.tabViewBinding { ItemTabBinding.inflate(layoutInflater) }
.tabLinkage { binding, isChoose, data, _ ->
binding.tvTabName.text = data.typeName
binding.viewBg.visibility = if (isChoose) View.VISIBLE else View.INVISIBLE
}
.build(this)
}
}
2.我们看看P层(MainPresenter)代码:
class MainPresenter : BasePresenter<List<TabNewsData>>() {
override fun setModel(): BaseModel<List<TabNewsData>> {
return MainModel() //持有M层的引用
}
}
2.我们看看M层(MainModel)代码:
class MainModel : BaseModel<List<TabNewsData>>() {
//使用List,记录请求的网络
private val callList= mutableListOf<Call<*>>()
override fun getHttpData(bundle: Bundle?) {
val tabNews = RetrofitManager.apiServer.getTabNews()
callList.add(tabNews)
tabNews.enqueue(MyCallback { state, message, dataList ->
Thread.sleep(1000)
postData(state, message, dataList)
})
}
override fun clear() {
callList.forEach {
it.cancel() //取消网络请求
}
callList.clear() //清除所有记录
super.clear()
}
}
4.我们看看RetrofitManager构建的请求代码:
object RetrofitManager {
private const val apiUrl="https://www.mxnzp.com/api/"
val apiServer: ApiService by lazy {
Retrofit.Builder()
.baseUrl(apiUrl)
.addConverterFactory(GsonConverterFactory.create())
.build().create(ApiService::class.java)
}
}
5.我们在看看ApiService使用的接口,返回网络请求的数据代码:
其中的mAppId,和mAppSecret是上一章,让小伙伴取申请的Id哦。
6.我们在看看MyCallback方法
//response.code() 常见码:https://blog.csdn.net/weixin_33775572/article/details/92356626
//2xx(成功):表示请求已成功被服务器接收、理解并处理。例如:
//200 OK:请求成功,服务器已返回请求的资源。
//201 Created:请求成功并创建了新的资源。
//204 No Content:请求成功,但服务器没有返回任何内容。
//3xx(重定向):表示请求的资源已被移动,需要客户端采取进一步的操作以完成请求。例如:
//301 Moved Permanently:资源已永久移动到新位置。
//302 Found:资源临时移动到新位置。
//304 Not Modified:资源未修改,客户端可以使用缓存的版本。
//4xx(客户端错误):表示请求包含错误,导致服务器无法处理。例如:
//400 Bad Request:请求无效或格式错误。
//401 Unauthorized:请求未授权,需要身份验证。
//403 Forbidden:服务器拒绝请求,即使客户端已认证。
//404 Not Found:请求的资源不存在。
//5xx(服务器错误):表示服务器在处理请求时发生错误,导致无法完成请求。例如:
//500 Internal Server Error:服务器内部错误,无法完成请求。
//503 Service Unavailable:服务不可用。
class MyCallback<T>(private val callBack: ((state: Int, message: String, dataList: T?) -> Unit)) :
Callback<BaseBean<T>> {
override fun onResponse(call: Call<BaseBean<T>>, response: Response<BaseBean<T>>) {
val body = response.body()
val data = body?.data
if (data != null) {
callBack.invoke(200, "请求数据成功", data)
} else {
callBack.invoke(response.code(), body?.msg.toString(), null)
}
}
override fun onFailure(call: Call<BaseBean<T>>, t: Throwable) {
callBack.invoke(404, "请检查您的网络链接情况,如网络无异常,则是访问的接口异常", null)
}
}
7.我们先看看MainActicity需要的TabNewsData
通过以上步骤,那么我们的首页MainAcitivity,就是实现了导航功能,解下我们去看看NewsFragment,子页面都做了什么呢。
二、NewsFragment中根据不同的类别
1.先看看布局代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvNewsList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:orientation="horizontal">
<Button
android:id="@+id/btnBreak"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:background="@drawable/base_shape_blue"
android:text="上一页"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPageNumber"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:background="@drawable/shape_blue_x"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="1"
android:textColor="@color/black"
android:textSize="18sp" />
<Button
android:id="@+id/btnNext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:background="@drawable/base_shape_blue"
android:text="下一页"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
2.看看Fragment中的代码:
class NewsFragment : BaseMVPFragment<FragmentNewsBinding,List<NewsListData>,NewsPresenter>() {
private val data by lazy { //获取传递过来的数据
MyBundleUtils.getKeyNumberData<TabNewsData>(requireArguments(), 0)!!
}
private var pager = 1 //页数
private val dataList = ObservableArrayList<NewsListData>() //数据变化的时候。RecyclerView会自动刷新数据
private var isLast = false //是否最后一页
private var isFirstGetData = true //是否第一次打开界面, 是的话获取第一页数据
override fun initData(savedInstanceState: Bundle?) {
binding.tvPageNumber.text = pager.toString() //设置现在是第几页
setOnClickListener(binding.btnBreak,binding.btnNext) //设置监听器
binding.rvNewsList.apply { //设置RecyclerView ,adapterd,当点击某一个Item时候,
//携带数据前往 NewsDetailsActivity界面
adapter = NewsAdapter(dataList).setClickCall { _, _, data, _ ->
val intent = Intent(requireActivity(), NewsDetailsActivity::class.java)
intent.putExtra("data",Gson().toJson(data))
requireActivity().startActivity(intent)
}
layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
}
}
override fun onResume() {
super.onResume()
if (isFirstGetData){
//当钱Tab可见的时候,判断是否第一次数据是否获
// 是就获取,否则不做处理
isFirstGetData=false
getNewsHttp() //请求某一页的数据
}
}
override fun myClick(v: View) { //当点击上一页或者下一页的时候,判断逻辑是否取获取数据
if (v.id == binding.btnBreak.id) {
if (pager == 1) {
"已经是第一页啦".showToast()
} else {
if (isLast) isLast = false
pager--
getNewsHttp()
}
} else {
if (isLast) {
"已经是最后一页啦".showToast()
} else {
pager++
getNewsHttp()
}
}
}
private fun getNewsHttp(){
LoadingDialogManager.loadingDialog(requireContext()) //启动弹出,告诉用户需要等待一下
presenter.startGetData(MyBundleUtils.setKeyData(data.typeId, pager)) //请求数据、传递参数typeId和页数
}
override fun dataCall(state: Int, message: String, dataList: List<NewsListData>?) {
LoadingDialogManager.loadOff() //关闭弹出,处理 数据
if (state==200 && !dataList.isNullOrEmpty()){
this.dataList.clear() //变更展示的数据数据
this.dataList.addAll(dataList)
}else if (state==200 && dataList!=null && dataList.isEmpty()){
isLast=true
"已经是最后一页啦".showToast()
pager--
}else{
message.showToast()
}
binding.tvPageNumber.text = pager.toString()
}
}
2.接下来,我们看看NewsPresenter(P)层代码:
class NewsPresenter : BasePresenter<List<NewsListData>>() {
override fun setModel(): BaseModel<List<NewsListData>> {
return NewsModel()
}
}
3.我们在看看NewsModel(M)层代码
class NewsModel : BaseModel<List<NewsListData>>() {
//请求网络的集合
private val callList = mutableListOf<Call<*>>()
companion object{ //这里用于缓存,已经请求过的数据,再一次获取的时候,不用再去从网络端获取了
private val newsMap = HashMap<String, List<NewsListData>>()
}
override fun getHttpData(bundle: Bundle?) {
val typeId = MyBundleUtils.getKeyNumberData<Int>(bundle!!, 0) //获取传递的数据
val pager = MyBundleUtils.getKeyNumberData<Int>(bundle, 1)
val newsListData = newsMap["${typeId}--${pager}"] //读取缓存数据,没有去获取往来款数据,否则直接使用本地数据
if (newsListData == null) {
val newsList = RetrofitManager.apiServer.getNewsList(typeId.toString(), pager.toString())
callList.add(newsList)
newsList.enqueue(MyCallback { state, message, dataList ->
if (state == 200 && dataList != null) {
newsMap["${typeId}--${pager}"] = dataList
}
postData(state, message, dataList)
})
} else {
postData(200, "优先获取缓存缓存", newsListData)
}
}
override fun clear() {
callList.forEach { it.cancel() }
callList.clear()
super.clear()
}
}
4.我们在看看需要展示的数据NewsAdapter:
class NewsAdapter(dataList: List<NewsListData>) :
BaseAdapter<NewsListData, ItemNewsBinding>(dataList) {
@SuppressLint("SetTextI18n")
override fun setItemDataView(binding: ItemNewsBinding, data: NewsListData, position: Int) {
//这里是保证标题一定有数据,没有便url时候,可以换成指定加载新闻图片
val url = if (data.imgList.isNullOrEmpty()) R.mipmap.icon_news else data.imgList[0]
GlideUtils.glideLoad(binding.ivNewsIcon,url,15)
binding.tvTitle.text=data.title
binding.tvContent.text="新闻来源:"+data.source+" 推送时间:"+data.postTime+"\n"+data.digest
binding.root.setOnClickListener {
clickItemCall?.invoke(it,position,data,0)
}
}
}
5.我们在看看Item布局ItemNewsBinding
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingTop="10dp"
android:paddingEnd="5dp"
android:paddingBottom="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivNewsIcon"
android:layout_width="120dp"
android:layout_height="120dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:ellipsize="end"
android:maxLines="1"
android:text="标题"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/ivNewsIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/ivNewsIcon"
app:layout_constraintTop_toTopOf="@+id/ivNewsIcon"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/tvContent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="5dp"
android:ellipsize="end"
android:maxLines="4"
android:paddingTop="5dp"
android:text="内容内容内容内容内容内容内容内容内容"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@+id/ivNewsIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/ivNewsIcon"
app:layout_constraintTop_toBottomOf="@+id/tvTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/qianse" />
</LinearLayout>
5.,接下来我们看看NewsListData:
三、接下来,我们看看NewsDetailsActivity,新闻详情界面:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:background="@color/blue"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivBack"
android:layout_width="23dp"
android:layout_height="31dp"
android:layout_centerVertical="true"
android:layout_marginStart="10dp"
android:paddingTop="8dp"
android:src="@mipmap/icon_break_white" />
</RelativeLayout>
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|left"
android:paddingStart="5dp"
android:paddingTop="8dp"
android:paddingEnd="5dp"
android:paddingBottom="10dp"
android:text="1212"
android:textColor="@color/black"
android:textSize="20sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvNewsDetailsList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never"
android:paddingBottom="15dp" />
</LinearLayout>
2.接下来我们看看代码NewsDetailsActivity具体代码:
class NewsDetailsActivity :
BaseMVPActivity<ActivityNewsDetailsBinding, NewsDetailsData, NewsDetailsPresenter>() {
override fun initData(savedInstanceState: Bundle?) {
//读取传递过来的数据
val newsListData = Gson().fromJson(intent.getStringExtra("data"), NewsListData::class.java)
binding.tvTitle.text = newsListData.title //设置标题
binding.ivBack.setOnClickListener { finish() } //点击返回
LoadingDialogManager.loadingDialog(this) //弹窗启动,获取数据
presenter.startGetData(MyBundleUtils.setKeyData(newsListData.newsId))
}
override fun dataCall(state: Int, message: String, dataList: NewsDetailsData?) {
LoadingDialogManager.loadOff() //关闭界面
if (dataList == null) message.showToast() //获取数据null,展示获取失败的消息
else binding.rvNewsDetailsList.apply { //否则则设置RecyclerView
adapter=NewsDetailsAdapter(dataList.items)
layoutManager=LinearLayoutManager(context,RecyclerView.VERTICAL,false)
}
}
override fun onDestroy() {
val adapter = binding.rvNewsDetailsList.adapter
if (adapter is NewsDetailsAdapter){ //销毁 NewsDetailsAdapter中播放视频的VideoView
adapter.stop()
}
super.onDestroy()
}
}
3.我们看看P层(NewsDetailsPresenter):
class NewsDetailsPresenter : BasePresenter<NewsDetailsData>() {
override fun setModel(): BaseModel<NewsDetailsData> {
return NewsDetailsModel()
}
}
4.我们在看看M层(NewsDetailsModel)
companion object {
//缓存数据,有则使用缓存数据,否则取网络获取数据
private val newsDetailsMap = HashMap<String, NewsDetailsData>()
}
private val callList = mutableListOf<Call<*>>()
override fun getHttpData(bundle: Bundle?) {
val keyNumberData = MyBundleUtils.getKeyNumberData<String>(bundle!!, 0).toString()
val newsDetailsData = newsDetailsMap[keyNumberData]
if (newsDetailsData == null) {
val newsDetails = RetrofitManager.apiServer.getNewsDetails(keyNumberData)
callList.add(newsDetails)
newsDetails.enqueue(MyCallback { state, message, dataList ->
if (dataList != null) newsDetailsMap[keyNumberData] = dataList
postData(state, message, dataList)
})
} else {
postData(200, "获取缓存数据", newsDetailsData)
}
}
override fun clear() {
callList.forEach { it.cancel() }
callList.clear()
super.clear()
}
}
5.接下来我们看看NewsDetailsData:
6.我们再看看NewsDetailsAdapter:
class NewsDetailsAdapter(dataList: List<NewsDetailsItemData>) :
BaseAdapter<NewsDetailsItemData, ItemNewsDetailsBinding>(dataList) {
private val videoViewList = mutableListOf<VideoView>()
override fun setItemDataView(binding: ItemNewsDetailsBinding, data: NewsDetailsItemData, position: Int) {
binding.root.setPadding(0, 0, 0, 0)
binding.root.setPadding(10, 10, 10, 10)
when (data.type) {
"text" -> {
binding.tvContent.visibility = View.VISIBLE
binding.ivImageUrl.visibility = View.GONE
binding.vvVideo.visibility = View.GONE
binding.tvContent.text = data.content
}
"img" -> {
binding.tvContent.visibility = View.GONE
binding.ivImageUrl.visibility = View.VISIBLE
binding.vvVideo.visibility = View.GONE
GlideUtils.glideLoad(binding.ivImageUrl, data.imageUrl)
}
"video" -> {
binding.tvContent.visibility = View.GONE
binding.ivImageUrl.visibility = View.GONE
binding.vvVideo.visibility = View.VISIBLE
if (data.videoUrl.isNullOrEmpty()) return
binding.vvVideo.setVideoPath(data.videoUrl[0]) // 设置视频视图的视频路径
val mc = MediaController(binding.vvVideo.context) // 创建一个媒体控制条
binding.vvVideo.setMediaController(mc) // 给视频视图设置相关联的媒体控制条
mc.setMediaPlayer(binding.vvVideo) // 给媒体控制条设置相关联的视频视图
binding.vvVideo.setOnPreparedListener {
binding.vvVideo.start()
}
binding.vvVideo.setOnErrorListener { _, _, _ ->
true
}
videoViewList.add(binding.vvVideo)
}
}
}
fun stop() {
videoViewList.forEach { it.stopPlayback() }
videoViewList.clear()
}
}
再看一下布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:padding="5dp">
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="17sp"
android:textColor="@color/black"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivImageUrl"
android:layout_width="match_parent"
android:layout_height="240dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
<VideoView
android:id="@+id/vvVideo"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
</RelativeLayout>
根据不同的类别,使用不同的展示。
整个app的代码,差不多就到这里了,接口数据又不懂的,参考上一章节的文章最后,有网址对接口说明的,我也是使用免费的api接口。