当前位置:   article > 正文

Android-kotlin开发新闻阅读App_kotlin新闻app

kotlin新闻app

@Android-kotlin开发新闻阅读类App

前言

各位小伙伴们大家好
之前一直是用java写安卓的程序,随着kotlin越来越完善,最近便自学了kotlin,学习之后,为了练手,便在工作之余,花了两个星期时间,运用之前工作中所积累的知识+自学的内容,制作了一个新闻类App,在这里记录一下开发过程,文末有源码下载链接,只需要将用户登录服务器数据库链接和API接口秘钥换成自己的即可。

项目概述

新闻阅读类App(kotlin):
1.API接口
2.欢迎页面加载,并判断是否登录过,从而决定跳转目标界面——SharedPreferences运用
3.登录注册界面,连接服务器数据库对用户信息注册,修改——反射连接服务器数据库
4.自定义控件,全局Toolbar,设置按钮,搜索控件,监听搜索框点击和搜索按钮,获取搜索内容——自定义View、SearchView控件
5.主页广告图轮播,点击监听——Banner控件
6.主页侧滑菜单,点击监听进行频道更改,主界面新闻列表内容刷新——NavigationView控件
7.主页新闻列表从接口读取新闻数据,按照自定义列表中设置的布局,分页加载,并且监听点击,跳转到新闻详情页——Retrofit、Paging3、自定义Recyclerview、Adapter、
8.主页新闻列表下拉刷新——SwipeRefreshLayout控件
9.新闻详情页,根据新闻ID去API接口读取具体HTML数据,并将其显示在Webview控件中,同时可点击自定义CheckBox进行收藏——折叠式标题栏、Webview、自定义CheckBox
10.新闻收藏,通过点击详情页自定义CheckBox,将新闻ID和频道ID,以及收藏用户名等存入Room数据库中——Room数据库
11.收藏列表,从Room数据库中读取收藏新闻数据,并且显示在自定义ListView上——自定义ListView

下面开始分步介绍

1.API接口

新闻数据使用的是阿里巴巴的新闻API接口,具体申请和使用方式
看这里:新闻API

2.欢迎页面加载

欢迎界面
加载登录界面,获取App的SharePreference中的用户数据,从而判断是否登录过,如果登陆过则直接跳转到主页,没有登陆过则跳转到登录界面

class WelcomeActivity : AppCompatActivity() {
    var handler = Handler()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //activity_welcome为欢迎界面布局
        setContentView(R.layout.activity_welcome)
        handler.postDelayed(Runnable {
        //获取App的SharePreference
            val sp = getSharedPreferences("user", MODE_PRIVATE)
            var DL = sp.getString("YHM", null) 
            if (DL == null) {
                DL = "首次登录"
            }
            if (DL =="首次登录") { //首次登录,跳转到登录界面
                intent = Intent(this, FirstActivity::class.java) //FirstActivity
                startActivity(intent)
                finish()
            } else { //之前登录过,直接跳转到主页
                intent = Intent(this, MainActivity::class.java) //FirstActivity
                startActivity(intent)
                finish()
            }
        }, 300)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

3.登录注册界面

登录界面
登录界面,点击注册按钮可以跳转到注册界面,运用反射Class.forName(…)连接服务器,界面基本布局和数据库方法调用具体请参考源码,这里主要说明通过反射的方法连接服务器数据库对用户信息进行增删改查的操作

class zengshangaicha {
    var m_con: Connection? = null
    @Throws(Exception::class)
    fun connect(): Int {
        W = 0
//连接服务器数据库
Class.forName("net.sourceforge.jtds.jdbc.Driver")
m_con = DriverManager.getConnection(
            "jdbc:jtds:sqlserver://服务器IP+端口号;DatabaseName=user1;charset=utf8;encrypt=true;" +
                    "trustServerCertificate=true", "数据库登录账号", "数据库登录密码"
        ) as Connection //
        Log.d("10457", "m_con=$m_con")
        return if (m_con != null) {
            1.also { W = it }
        } else {
            0.also { W = it }
        }
    }
//新用户注册
    @Throws(Exception::class)
    fun insert(
        count: String?,
        password: String?,
        phonenum: String?,
        Address: String?
    ) { //用户名,密码,联系电话,地址
        if (W == 1) {
            val sql = "insert into user1.dbo.[user](count,password,guanliyuan,address,telnumber) values(?,?,?,?,?)" //普通用户
            val A1 = m_con!!.prepareStatement(sql) as PreparedStatement
            try {
                A1.setString(1, count)
                A1.setString(2, password)
                A1.setInt(3, 1)
                A1.setString(4, Address)
                A1.setString(5, phonenum)
                A1.executeUpdate()
                A1.close()
            } catch (e: Exception) {
                Log.d("10456", "异常$e")
                throw Exception("操作中出现错误!!!")
            } finally {
                m_con!!.close()
            }
        } else {
            Log.d("10456", "数据库未连接")
        }
    }
//查找用户
    @Throws(Exception::class)
    fun select(count: String?, password: String?): String { //查,用于用户登录注册验证,查执行过程中m_con不用关闭
        connect()
        return if (W == 1) {
            val sql2 = "select * from user1.dbo.[user] where (count=?)"
            val A1 = m_con!!.prepareStatement(sql2) as PreparedStatement
            Log.d("10546", "A1:$A1")
            try {
                A1.setString(1, count)
                val r1 = A1.executeQuery()
                if (r1.next()) {
                    val pass = r1.getString(2)
                    pass //
                } else { //没有查到
                    "-1"
                }
            } catch (e: Exception) {
                throw Exception("操作中出现错误!!!")
            } finally {
            }
        } else {
            "0"
        }
    }
//用户信息修改
    @Throws(Exception::class)
    fun change(
        count: String?,
        password: String?,
        telnum: String?,
        address: String?
    ): Int { //用户信息密码重置
        Log.d("10456", "进入密码重置功能")
        return if (W == 1) {
            val sql3 = "update user1.dbo.[user] set password=? address=? telnumber=? where count=?"
            Log.d("10456", "sql3定义")
            val A1 = m_con!!.prepareStatement(sql3)
            try {
                Log.d("10456", "准备加载b")
                A1.setString(1, password) //setString只是索引定位sql3语句中定位,第一个问号赋值password
                A1.setString(2, telnum) //
                A1.setString(3, address)
                A1.setString(4, count) //第四个问号赋值count
                Log.d("10456", "将第二列密码值修改为password值")
                val b = A1.executeUpdate() //b为受影响数据条数
                Log.d("10456", "b=$b")
                1 //重置完成
            } catch (e: Exception) {
                throw Exception("操作中出现错误!!!")
            } finally {
                m_con!!.close()
            }
        } else {
            Log.d("10456", "数据库未连接")
            0
        }
    }
//版本判断    
    companion object {
        var W = 0
        @JvmStatic
        fun main(args: Array<String>) {
            if (Build.VERSION.SDK_INT > 9) {
                val policy = ThreadPolicy.Builder().permitAll().build()
                StrictMode.setThreadPolicy(policy)
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117

登录成功后,将用户名和密码存入轻量数据库SharedPreferences的user中,方便程序中调用用户信息,并跳转到主界面

 val sp1 = getSharedPreferences("user", MODE_PRIVATE)
                        sp1.edit().putString("YHM", name).commit()
                        sp1.edit().putString("MM", pass).commit()
                        val intent=Intent(this,MainActivity::class.java)
                        startActivity(intent)
                        finish()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4.自定义全局Toolbar(自定义控件)

自定义View
<1>.将不同页面中要用到的共同控件放在自定义控件的布局activity_navigate_view.xml中

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="50dp"
    android:background="#00000000" >
    <LinearLayout
        android:id="@+id/navigate_bg"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#CA381F"
        android:weightSum="7"
        android:orientation="horizontal">
        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="2dp"
            android:layout_marginRight="2dp"
            android:layout_weight="1">
            <!--左侧按钮,用于点击监听加载侧滑菜单列表或者返回-->
            <Button
                android:id="@+id/leftBtn"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_centerInParent="true"
                android:background="@drawable/translucentbutton"
                android:textColor="#000000"
                android:textSize="14sp" />
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_weight="5">
			<!--中间搜索框-->
            <androidx.appcompat.widget.SearchView
                android:id="@+id/SearchE"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_centerInParent="true"
                android:layout_marginStart="5dp"
                android:layout_marginTop="5dp"
                android:layout_marginEnd="5dp"
                android:layout_marginBottom="5dp"
                android:background="@drawable/translucent"
                android:queryHint="请输入检索内容"></androidx.appcompat.widget.SearchView>
        </RelativeLayout>
        <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_weight="5">
        <!--中间标题栏-->
        <TextView
                android:id="@+id/TM"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="空名称"
                android:layout_weight="5"
                android:textColor="#000000"
                android:textSize="20sp" />
    </RelativeLayout>
        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="2dp"
            android:layout_marginRight="2dp"
            android:layout_weight="1">
            <!--右侧按钮-->
            <Button
                android:id="@+id/rightBtn"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_centerInParent="true"
                android:textColor="#000000"
                android:textSize="14sp"
                android:layout_weight="1"
                android:background="@drawable/translucentbutton"
                />
        </RelativeLayout>

    </LinearLayout>
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

<2>.通过自定义控件NavigateViewnew.kt来加载activity_navigate_view.xml布局,并定义布局中不同控件可见/不可见方法

class NavigateViewnew : RelativeLayout {


    constructor(context: Context?) : super(context) {}
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        LayoutInflater.from(context).inflate(
            R.layout.activity_navigate_view, this,
            true
        )
    }

    fun setLeftHideBtn(boo: Boolean) {
        setViewHide(leftBtn, boo)
    }

    fun setRightHideBtn(boo: Boolean) {
        setViewHide(rightBtn, boo)
    }

    fun setTM(boo: Boolean) {
        setViewHide(TM, boo)
    }

    fun setSearchE(boo: Boolean) {
        setViewHide(SearchE, boo)
    }


    private fun setViewHide(view: View?, boo: Boolean) {
        if (boo) {
            view!!.visibility = GONE
        } else {
            view!!.visibility = VISIBLE
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

<3>.单独定义一个布局文件toolbar.xml来放自定义控件NavigateViewnew

<?xml version="1.0" encoding="utf-8"?>
<com.example.news.view.NavigateViewnew xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/toolbar">

</com.example.news.view.NavigateViewnew>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

<4>.在基类BaseActivity.kt中定义ToolBarview方法,通过定位toolbar来具体调用NavigateViewnew.kt中编写的控件方法,从而达到控制控件可见或者不可见的目的

 protected lateinit var navigete:NavigateViewnew
 public fun ToolBarview(Lbtn:String?, Search:String?, Title: String?, Rbtn:String?){
        navigete=findViewById(R.id.toolbar);
        if(Lbtn==null){
            navigete.setLeftHideBtn(true);
        }else{
            navigete.setLeftHideBtn(false);
            navigete.leftBtn.text=Lbtn
            if(Lbtn.equals("返回")){
                navigete.leftBtn.setOnClickListener {
                    finish()
                }
            }
        }
        if(Search==null){
            navigete.setSearchE(true)
        }else{
            navigete.setSearchE(false)
        }
        if(Title==null){
            navigete.setTM(true)
        }else{
            navigete.setTM(false)
            navigete.TM.text=Title
        }
        if(Rbtn==null){navigete.setRightHideBtn(true)}
        else{
            navigete.setRightHideBtn(false)
            navigete.rightBtn.text=Rbtn}
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

<5>.不同的类文件使用该自定义控件时,只需要在对应的布局文件中引入toolbar.xml,类继承BaseActivity.kt,调用ToolBarview方法即可

//引入toolbar.xml
<include layout="@layout/toolbar"/>
//类继承BaseActivity.kt
class MainActivity : BaseActivity() {}
//调用ToolBarview方法
ToolBarview("频道","2",null,null)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

<6>.SearchView控件使用
(1)xml布局文件中

<androidx.appcompat.widget.SearchView
                android:id="@+id/SearchE"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_centerInParent="true"
                android:layout_marginStart="5dp"
                android:layout_marginTop="5dp"
                android:layout_marginEnd="5dp"
                android:layout_marginBottom="5dp"
                android:background="@drawable/translucent"
                android:queryHint="请输入检索内容"></androidx.appcompat.widget.SearchView>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

(2)Activity中监听点击弹出软键盘,搜索按钮点击获取输入内容

 //Search搜索框点击监听
        SearchE.setOnClickListener{
            //弹出软键盘
            SearchE.setIconified(false);
        }
        //Search外部搜索按钮点击监听
        SearchE.setOnQueryTextListener(object : OnQueryTextListener {
            override fun onQueryTextChange(queryText: String): Boolean {
                return true
            }
            override fun onQueryTextSubmit(queryText: String): Boolean {
                //点击键盘上搜索按钮获取到输入的要搜索的内容,可去对应服务器数据库中或者接口中对内容进行搜索
                Toast.makeText(context,"搜索功能正在开发中...,您输入的搜索内容是:"+queryText,Toast.LENGTH_LONG).show()
                return true
            }
        })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

5.主页广告图轮播(Banner控件)

轮播图
轮播图根据个人需要可要可不要,在此只是说明一下轮播控件Banner用法
<1>xml布局文件中

 <com.youth.banner.Banner
                    android:id="@+id/banner"
                    android:layout_width="match_parent"
                    android:layout_height="180dp" />
  • 1
  • 2
  • 3
  • 4

<2>Activity中设置轮播图网页连接,以及监听点击

//banner图片地址
    private var list= mutableListOf<String>(
        "https://img.zcool.cn/community/013de756fb63036ac7257948747896.jpg",
        "https://img.zcool.cn/community/01639a56fb62ff6ac725794891960d.jpg",
        "https://img.zcool.cn/community/01270156fb62fd6ac72579485aa893.jpg",
        "https://img.zcool.cn/community/01233056fb62fe32f875a9447400e1.jpg",
        "https://img.zcool.cn/community/016a2256fb63006ac7257948f83349.jpg"
    )
  //加载轮播图
var banner: Banner<String, BannerImageAdapter<String>> = findViewById(R.id.banner)
banner.setAdapter(object : BannerImageAdapter<String>(list) {
override fun onBindView(holder: BannerImageHolder, data: String, position: Int, size: Int) {
                //图片加载自己实现
                Glide.with(holder.itemView)
                    .load(data)
                    .apply(RequestOptions.bitmapTransform(RoundedCorners(30)))
                    .into(holder.imageView) }
        }).addBannerLifecycleObserver(this).setIndicator(CircleIndicator(this))
//轮播图点击监听
banner.setOnBannerListener { data, position ->
            Toast.makeText(this,"广告位招租中,暂时未开放",Toast.LENGTH_LONG).show()
        }    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

6.主页侧滑菜单(NavigationView控件)

侧滑菜单
本来想将新闻频道做成Tablayout+Viewpager+FragmentAdapter的切换模式,但是因为采集接口的频道过多,于是采用了侧滑菜单点击监听刷新新闻列表的模式,NavigationView控件具体代码如下:
<1>xml文件中

<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.news.MainActivity"
    android:id="@+id/drawerLayout"
    android:layout_marginLeft="1dp"
    android:layout_marginRight="1dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="50dp"
        android:orientation="vertical">
        <!--主界面内容-->
       
        
</LinearLayout>
    <!--侧滑菜单界面内容,nav_menu和nav_header内容参见代码-->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navView"
        android:layout_width="250dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

<2>Activity中内容

//侧滑菜单添加监听器
private val listener = object : DrawerLayout.DrawerListener {

        override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        }
        //当菜单界面打开情况下,打开手势滑动开关,可通过侧滑关闭侧面菜单
        override fun onDrawerOpened(drawerView: View) {
            drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) //打开手势滑动

        }
        //当菜单界面关闭情况下,关闭手势滑动开关,只能通过点击上方频道按钮打开菜单栏
        override fun onDrawerClosed(drawerView: View) {
            drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
        }

        override fun onDrawerStateChanged(newState: Int) {
        }
    }
 //设置初始状态手势滑动关闭,菜单隐藏,添加配置好的监听器
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
drawerLayout.addDrawerListener(listener)
drawerLayout.closeDrawers()
//侧滑菜单点击监听,获取到点击内容,点击内容不同做不同反馈
navView.setNavigationItemSelectedListener {
            var name=it.toString()
            drawerLayout.closeDrawers()
            true
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

7.主页新闻列表(Retrofit、Paging3、自定义Recyclerview)

新闻列表
使用Paging3设置要从接口读出数据的具体页数、每页条数等基本数据,使用Retrofit从接口中读取出数据,解析成新闻对象,显示在自定义Recyclerview列表中,并对列表实现点击监听,携带新闻ID和频道ID跳转到新闻详情页
<1>Retrofit+Paging3,原理参考这里:Retrofit+Paging3
<2>具体代码可参考源码中paging3plusretrofit包中的代码,这里只对主要代码进行说明
(1)Retrofit

interface GitHubService {
//设置API请求的基本参数,并且将返回数据解析成新闻类型RepoResponse
    @Headers("Authorization:APPCODE 申请的秘钥")
    @GET("/newsList?")
    suspend fun searchRepos(@Query("channelId") channelId: String, @Query("channelName") channelName: String,
                            @Query("id") id: String, @Query("needAllList") needAllList: String
        ,@Query("needContent") needContent: String, @Query("needHtml") needHtml: String,
        @Query("title") title: String,@Query("page") page: String, @Query("maxResult") maxResult: String): RepoResponse

    companion object {
        private const val BASE_URL = "http://ali-news.showapi.com"
//按照设置好的数据对API接口进行请求
        fun create(): GitHubService {
            val okHttpClient = OkHttpClient.Builder()
                // 给Client 添加拦截器HandleErrorInterceptor(),在拦截器中对收到的数据进行提前处理
                .addInterceptor(HandleErrorInterceptor())
                .build()
            return Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(okHttpClient)
                    .build()
                    .create(GitHubService::class.java)
        }



    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

(2)Paging3

class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
        return try {
        //设置请求的基本参数
            val channelId= App.key//频道ID
            Log.d("1725111","App.key="+channelId)
            val channelName=""//频道名称
            val id=""//新闻ID
            val needAllList="0"//是否需要返回图片以及段落属性
            val needContent="0"//是否返回正文
            val needHtml="0"//是否返回HTML格式
            val title=""//标题名称
            val page = params.key?: 1// 当前第几页
            val maxResult = params.loadSize//每页多少条数据
            val page1=page.toString()
            val maxResult1=maxResult.toString()
            //从API接口读取数据
            val repoResponse = gitHubService.searchRepos(channelId,channelName,id,needAllList,needContent,needHtml,title,page1,maxResult1)
            val repoItems = repoResponse.items
            val prevKey = if (page > 1) page - 1 else null
            val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
            //对处理好的新闻数据进行分页处理
            LoadResult.Page(repoItems, prevKey, nextKey)
        } catch (e: Exception) {
            e.printStackTrace()
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

(3)Activity中调用

 //新闻数据分页加载,recycler_view为自定义的列表
recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
        lifecycleScope.launch {
            viewModel.getPagingData().collect { pagingData ->
                repoAdapter.submitData(pagingData)
            }
        }
//加载等待循环条和数据列表Recycleview根据状态不同切换显示
repoAdapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.NotLoading -> {
                    progress_bar.visibility = View.INVISIBLE
                    recycler_view.visibility = View.VISIBLE
                }
                is LoadState.Loading -> {
                    progress_bar.visibility = View.VISIBLE
                    recycler_view.visibility = View.INVISIBLE
                }
                is LoadState.Error -> {
                    val state = it.refresh as LoadState.Error
                    progress_bar.visibility = View.INVISIBLE
                    Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
//下拉刷新列表  swipeRefresh为下拉刷新列表控件
//repoAdapter为新闻列表适配器
swipeRefresh.setOnRefreshListener {
            repoAdapter.refresh()
            swipeRefresh.isRefreshing = false
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

8. 主页新闻列表下拉刷新(SwipeRefreshLayout)

<1>xml文件中

 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
                    android:id="@+id/swipeRefresh"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                   app:layout_behavior="@string/appbar_scrolling_view_behavior">
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:orientation="vertical">
                       <!--主页内容-->
                    </LinearLayout>
                </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

<2>Activity中

//Recyclerview列表下拉刷新
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
        swipeRefresh.setOnRefreshListener {
            //具体内容...
            swipeRefresh.isRefreshing = false
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

9.新闻详情页(折叠式标题栏+Webview+自定义CheckBox)

新闻详情页使用折叠式标题栏,根据传入的新闻ID和频道ID去API接口读取具体的新闻HTML数据,并将其显示在Webview控件中,同时可点击自定义CheckBox进行收藏
<1>折叠式标题栏
(1)xml内容

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.news.ContentActivity"
    android:layout_height="match_parent">
    <!--标题栏:新闻背景图片+toolbar-->
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp">
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:contentScrim="@color/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:id="@+id/ImageView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax" />
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <!--内容栏-->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="@color/black"
                android:layout_gravity="center_vertical"
                android:textSize="20sp"
                android:text="ZZZZ"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:layout_marginTop="2dp"
                android:id="@+id/contenttitle"/>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="30dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:layout_marginTop="5dp"
                android:weightSum="5">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:text="来源:"
                    android:textSize="15sp"
                    android:layout_weight="1"
                    android:textColor="@color/black"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:id="@+id/contentzuozhe"
                    android:textSize="15sp"
                    android:gravity="left"
                    android:layout_weight="2"
                    android:textColor="@color/black"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:text="收藏:"
                    android:gravity="right"
                    android:textSize="15sp"
                    android:layout_weight="1"
                    android:textColor="@color/black"/>
                <CheckBox
                    android:button="@null"
                    android:id="@+id/ft_cb"
                    android:background="@drawable/check_style"
                    android:layout_weight="1"
                    android:gravity="center_vertical"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:layout_marginTop="5dp"
                android:weightSum="5">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:text="时间:"
                    android:textSize="15sp"
                    android:layout_weight="1"
                    android:textColor="@color/black"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:id="@+id/contenttime"
                    android:textSize="15sp"
                    android:gravity="left"
                    android:layout_weight="2"
                    android:textColor="@color/black"/>
            </LinearLayout>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="3dp"
                android:layout_marginTop="3dp"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:background="#D3D1D1"></TextView>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <WebView
                    android:id="@+id/content"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="5dp"
                    android:layout_marginLeft="10dp"
                    android:layout_marginRight="10dp"
                    android:scrollbars="vertical"
                    android:layout_marginTop="5dp"
                    android:textColor="#272525"
                    android:text=""
                    android:textSize="15sp"
                    />
            </LinearLayout>

        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144

(2)Activity中添加标题栏

//设置折叠标题栏
        setSupportActionBar(toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.setHomeButtonEnabled(true)
        collapsingToolbar.title ="新闻内容"
        val myIntend = intent
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

<2>WebView显示读取的HTML数据
在主页做新闻列表数据读取时,如果带每条新闻的具体HTML内容读取,则数据量比较大,传输速度会降低,于是从主页跳转到详情页时传入要观看新闻的新闻ID和频道ID,再显示在页面中
(1)从接口读取HTML数据

//根据新闻key和频道key来获取具体新闻内容,并且显示在界面上
    fun CZXS(key:String,channelID:String){
        thread {
            val host = "https://ali-news.showapi.com/newsList?"
            val appcode = "申请的接口密钥"
            val headers: MutableMap<String, String> = HashMap()
            //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
            //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
            headers["Authorization"] = "APPCODE $appcode"
            val querys: MutableMap<String?, Any?> = HashMap()
            querys["channelId"] = channelID
            querys["channelName"] = ""
            querys["id"] =key
            querys["maxResult"] = "20"
            querys["needAllList"] = "0"
            querys["needContent"] = "0"
            querys["needHtml"] = "1"
            querys["page"] = "1"
            querys["title"] = ""
            try {
                val response: String?= HttpUtils.getRequest(host,headers,querys)
                var M="["+ StringEscapeUtils.unescapeJavaScript(response)+"]"
                var jsonArray = JSONArray(M)
                var jsonObject = jsonArray.getJSONObject(0)
                var showapi_res_body = jsonObject.getString("showapi_res_body")
                if(!(showapi_res_body.equals("null"))){
                    showapi_res_body="["+ StringEscapeUtils.unescapeJavaScript(showapi_res_body)+"]"
                    jsonArray = JSONArray(showapi_res_body)
                    jsonObject = jsonArray.getJSONObject(0)
                    var pagebean= jsonObject.getString("pagebean")
                    pagebean= "["+ StringEscapeUtils.unescapeJavaScript(pagebean)+"]"
                    jsonArray = JSONArray(pagebean)
                    jsonObject = jsonArray.getJSONObject(0)
                    var contentlist=jsonObject.getString("contentlist")
                    var data= StringEscapeUtils.unescapeJavaScript(contentlist)
                    val gson = Gson()
                    val S=data
                    val typeOf = object : TypeToken<List<Repo>>() {}.type
                    val people = gson.fromJson<List<Repo>>(S, typeOf)
                    val repo=people.get(0)
                    //在界面上显示数据
                    showResponse(repo)
                }
            }
            catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

(2)在界面上显示数据

//在界面线程中显示数据
    fun showResponse(repo:Repo){
        runOnUiThread {
            RRepo=repo
            if(repo.img==null){
               ImageView.setImageResource(R.drawable.beixuan)
            }else{
                Picasso.with(context).load(repo.img).error(R.drawable.beixuan).into(ImageView)
            }
            contenttitle.text=repo.title
            contentzuozhe.text=repo.source
            contenttime.text=repo.pubDate
            content.loadDataWithBaseURL(null, HtmlFormat.getNewContent(repo.html.toString()), "text/html", "utf-8", null);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

<3>自定义Checkbox,心形收藏,勾选为红心,不勾选为白心
(1)xml中

<CheckBox
                    android:button="@null"
                    android:id="@+id/ft_cb"
                    android:background="@drawable/check_style"
                    android:layout_weight="1"
                    android:gravity="center_vertical"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(2)check_style.xml,其中selected和selectedfalse为自定义的图片素材

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--    选中状态-->
    <item android:drawable="@drawable/selected" android:state_checked="true"/>
    <!--    不选中状态-->
    <item android:drawable="@drawable/selectedfalse" android:state_checked="false"/>
    <item android:drawable="@drawable/selected" android:state_pressed="true"/>
    <!--    默认状态-->
    <item android:drawable="@drawable/selectedfalse"/>
</selector>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

10.新闻收藏

通过点击详情页自定义CheckBox(),将新闻ID和频道ID,以及收藏用户名等基本信息存入Room数据库中
<1>Room数据库基本设置
(1)NewStar.kt

@Entity(tableName = "NewStar")
data class NewStar(var user: String, var title: String, var pic1: String
                ,var date: String, var source: String, var keyID: String,var channelID:String) {
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(2)AppDatabase.kt

@Database(version = 2, entities = [NewStar::class])
abstract class AppDatabase : RoomDatabase() {
    abstract fun NewStarDao(): NewStarDao
    companion object {
        private var instance: AppDatabase? = null
        @Synchronized
        fun getDatabase(context: Context): AppDatabase {
            instance?.let {
                return it
            }
            return Room.databaseBuilder(
                App.context,
                AppDatabase::class.java, "app_database")
                .build().apply {
                    instance = this
                }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(3)NewStarDao.kt

@Dao
interface NewStarDao {
    //增
    @Insert
    fun insertUser(user: NewStar): Long
    //查找全部
    @Query("select * from NewStar where user = :user")
    fun loadAllUsers(user:String): List<NewStar>
    //查   ID相同  user相同
    @Query("select * from NewStar where keyID = :ID and user=:user")
    fun loadUsersOlderThan(ID: String,user:String): List<NewStar>
    //删  ID相同  user相同
    @Query("delete from NewStar where keyID = :ID and user=:user")
    fun deleteUserByLastName(ID: String,user:String): Int

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

<2>进入详情页先去数据库中查询是否收藏过该新闻,如果收藏过则checkbox在从一开始为勾选状态;通过点击自定义CheckBox存入Room数据库/从Room数据库删除

 //加载页面初期先根据user和ID去数据库查找,如果有则标记为收藏
val newStarDao = AppDatabase.getDatabase(this).NewStarDao()
        thread {
            val l1=newStarDao.loadUsersOlderThan(key,Dangqianuser)
            if(l1.size>0){ft_cb.isChecked=true}
        }
 //收藏按钮状态改变监听,
        // 如果状态由未收藏转为收藏,去数据库里找,如果没有则写入数据库
        //如果由收藏转为取消收藏,去数据库里找,如果有则删除
        ft_cb.setOnCheckedChangeListener(object :CompoundButton.OnCheckedChangeListener{
            override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
                if(isChecked){
                    thread {
                        val l1=newStarDao.loadUsersOlderThan(key,Dangqianuser)
                        if(!(l1.size>0)){
                            var newstar=NewStar(Dangqianuser,RRepo.title,RRepo.img,RRepo.pubDate.toString(),RRepo.source,RRepo.id,RRepo.channelId.toString())
                            val M=newStarDao.insertUser(newstar)
                            Log.d("1745361","M="+ M)
                            if(M>0){
                                ft_cb.isChecked=true
                                Looper.prepare()
                                Toast.makeText(context,"收藏成功!",Toast.LENGTH_LONG).show()
                                Looper.loop()
                            }
                        }
                    }
                }
                else{
                    thread {
                        val l1=newStarDao.loadUsersOlderThan(key,Dangqianuser)
                        if(l1.size>0){
                            val M=newStarDao.deleteUserByLastName(key,Dangqianuser)
                            if(M>0){
                                Looper.prepare()
                                Toast.makeText(context,"取消收藏成功!",Toast.LENGTH_LONG).show()
                                Looper.loop()
                            }
                        }
                    }
                }
            }
        })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

11.收藏列表

收藏列表
点击侧滑菜单中的收藏按钮,进入到收藏列表页面,去Room数据库中查找收藏的新闻信息,并且显示在自定义的Listview中,并且监听新闻列表点击,跳转到新闻详情页,自定义Listview及适配器请参看具体代码

//listciew加载收藏列表并且点击跳转
        thread {
            list=newStarDao.loadAllUsers(Dangqianuser)
            if(list.size>0){
                adapter=NewStarAdapter(this,R.layout.repo_item,list)
                LshouCang.adapter=adapter
            }else{
                Looper.prepare()
                Toast.makeText(App.context,"并未检测到收藏数据!!", Toast.LENGTH_LONG).show()
                Looper.loop()
            }
        }
        //带参数跳转到内容详情页  新闻ID+频道ID
        LshouCang.setOnItemClickListener{parent,view,position,id->
            val newstar=list[position]
            val intent=Intent(context,ContentActivity::class.java)
            intent.putExtra("newkey",newstar.keyID)
            intent.putExtra("channelkey",newstar.channelID)
            startActivity(intent)
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

至此,项目介绍完毕,文中布局和图标等可随需求更改,第一次写博文,手生,写的不好,还望各路大神海涵,如有错误,还请指正。
转载请注明来源,谢谢。
完整代码下载链接:源码

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/803047
推荐阅读
  

闽ICP备14008679号