android、鸿蒙开发--第十三章-->Android MVP 新闻App

一、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接口。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值