Android-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的SharePreferenceval sp = getSharedPreferences("user", MODE_PRIVATE)var DL = sp.getString("YHM", null) if (DL == null) {DL = "首次登录"}if (DL =="首次登录") { //首次登录,跳转到登录界面intent = Intent(this, FirstActivity::class.java) //FirstActivitystartActivity(intent)finish()} else { //之前登录过,直接跳转到主页intent = Intent(this, MainActivity::class.java) //FirstActivitystartActivity(intent)finish()}}, 300)}
}
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 PreparedStatementtry {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 PreparedStatementLog.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语句中定位,第一个问号赋值passwordA1.setString(2, telnum) //A1.setString(3, address)A1.setString(4, count) //第四个问号赋值countLog.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@JvmStaticfun main(args: Array<String>) {if (Build.VERSION.SDK_INT > 9) {val policy = ThreadPolicy.Builder().permitAll().build()StrictMode.setThreadPolicy(policy)}}}
}
登录成功后,将用户名和密码存入轻量数据库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()
4.自定义全局Toolbar(自定义控件)
<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" ><LinearLayoutandroid:id="@+id/navigate_bg"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#CA381F"android:weightSum="7"android:orientation="horizontal"><RelativeLayoutandroid:layout_width="wrap_content"android:layout_height="match_parent"android:layout_marginLeft="2dp"android:layout_marginRight="2dp"android:layout_weight="1"><!--左侧按钮,用于点击监听加载侧滑菜单列表或者返回--><Buttonandroid: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><RelativeLayoutandroid:layout_width="wrap_content"android:layout_height="match_parent"android:layout_weight="5"><!--中间搜索框--><androidx.appcompat.widget.SearchViewandroid: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><RelativeLayoutandroid:layout_width="wrap_content"android:layout_height="match_parent"android:layout_weight="5"><!--中间标题栏--><TextViewandroid: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><RelativeLayoutandroid:layout_width="wrap_content"android:layout_height="match_parent"android:layout_marginLeft="2dp"android:layout_marginRight="2dp"android:layout_weight="1"><!--右侧按钮--><Buttonandroid: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>
<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}}
}
<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>
<4>.在基类BaseActivity.kt中定义ToolBarview方法,通过定位toolbar来具体调用NavigateViewnew.kt中编写的控件方法,从而达到控制控件可见或者不可见的目的
protected lateinit var navigete:NavigateViewnewpublic 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=Lbtnif(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}}
<5>.不同的类文件使用该自定义控件时,只需要在对应的布局文件中引入toolbar.xml,类继承BaseActivity.kt,调用ToolBarview方法即可
//引入toolbar.xml
<include layout="@layout/toolbar"/>
//类继承BaseActivity.kt
class MainActivity : BaseActivity() {}
//调用ToolBarview方法
ToolBarview("频道","2",null,null)
<6>.SearchView控件使用
(1)xml布局文件中
<androidx.appcompat.widget.SearchViewandroid: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>
(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}})
5.主页广告图轮播(Banner控件)
轮播图根据个人需要可要可不要,在此只是说明一下轮播控件Banner用法
<1>xml布局文件中
<com.youth.banner.Bannerandroid:id="@+id/banner"android:layout_width="match_parent"android:layout_height="180dp" />
<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()}
6.主页侧滑菜单(NavigationView控件)
本来想将新闻频道做成Tablayout+Viewpager+FragmentAdapter的切换模式,但是因为采集接口的频道过多,于是采用了侧滑菜单点击监听刷新新闻列表的模式,NavigationView控件具体代码如下:
<1>xml文件中
<androidx.drawerlayout.widget.DrawerLayoutxmlns: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"><LinearLayoutandroid: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.NavigationViewandroid: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>
<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}
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): RepoResponsecompanion 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)}}}
(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//频道IDLog.d("1725111","App.key="+channelId)val channelName=""//频道名称val id=""//新闻IDval 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.itemsval prevKey = if (page > 1) page - 1 else nullval 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}
(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.INVISIBLErecycler_view.visibility = View.VISIBLE}is LoadState.Loading -> {progress_bar.visibility = View.VISIBLErecycler_view.visibility = View.INVISIBLE}is LoadState.Error -> {val state = it.refresh as LoadState.Errorprogress_bar.visibility = View.INVISIBLEToast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()}}}
//下拉刷新列表 swipeRefresh为下拉刷新列表控件
//repoAdapter为新闻列表适配器
swipeRefresh.setOnRefreshListener {repoAdapter.refresh()swipeRefresh.isRefreshing = false}
8. 主页新闻列表下拉刷新(SwipeRefreshLayout)
<1>xml文件中
<androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:id="@+id/swipeRefresh"android:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="@string/appbar_scrolling_view_behavior"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><!--主页内容--></LinearLayout></androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<2>Activity中
//Recyclerview列表下拉刷新swipeRefresh.setColorSchemeResources(R.color.colorPrimary)swipeRefresh.setOnRefreshListener {//具体内容...swipeRefresh.isRefreshing = false}
9.新闻详情页(折叠式标题栏+Webview+自定义CheckBox)
使用折叠式标题栏,根据传入的新闻ID和频道ID去API接口读取具体的新闻HTML数据,并将其显示在Webview控件中,同时可点击自定义CheckBox进行收藏
<1>折叠式标题栏
(1)xml内容
<androidx.coordinatorlayout.widget.CoordinatorLayoutxmlns: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.AppBarLayoutandroid:id="@+id/appBar"android:layout_width="match_parent"android:layout_height="250dp"><com.google.android.material.appbar.CollapsingToolbarLayoutandroid: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"><ImageViewandroid:id="@+id/ImageView"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop"app:layout_collapseMode="parallax" /><androidx.appcompat.widget.Toolbarandroid: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.NestedScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="@string/appbar_scrolling_view_behavior"><LinearLayoutandroid:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid: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"/><LinearLayoutandroid: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"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:text="来源:"android:textSize="15sp"android:layout_weight="1"android:textColor="@color/black"/><TextViewandroid: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"/><TextViewandroid: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"/><CheckBoxandroid: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><LinearLayoutandroid: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"><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:text="时间:"android:textSize="15sp"android:layout_weight="1"android:textColor="@color/black"/><TextViewandroid: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><TextViewandroid:layout_width="match_parent"android:layout_height="3dp"android:layout_marginTop="3dp"android:layout_marginLeft="10dp"android:layout_marginRight="10dp"android:background="#D3D1D1"></TextView><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><WebViewandroid: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>
(2)Activity中添加标题栏
//设置折叠标题栏setSupportActionBar(toolbar)supportActionBar?.setDisplayHomeAsUpEnabled(true)supportActionBar?.setHomeButtonEnabled(true)collapsingToolbar.title ="新闻内容"val myIntend = intent
<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 83359fd73fe94948385f570e3c139105headers["Authorization"] = "APPCODE $appcode"val querys: MutableMap<String?, Any?> = HashMap()querys["channelId"] = channelIDquerys["channelName"] = ""querys["id"] =keyquerys["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=dataval typeOf = object : TypeToken<List<Repo>>() {}.typeval people = gson.fromJson<List<Repo>>(S, typeOf)val repo=people.get(0)//在界面上显示数据showResponse(repo)}}catch (e: Exception) {e.printStackTrace()}}}
(2)在界面上显示数据
//在界面线程中显示数据fun showResponse(repo:Repo){runOnUiThread {RRepo=repoif(repo.img==null){ImageView.setImageResource(R.drawable.beixuan)}else{Picasso.with(context).load(repo.img).error(R.drawable.beixuan).into(ImageView)}contenttitle.text=repo.titlecontentzuozhe.text=repo.sourcecontenttime.text=repo.pubDatecontent.loadDataWithBaseURL(null, HtmlFormat.getNewContent(repo.html.toString()), "text/html", "utf-8", null);}}
<3>自定义Checkbox,心形收藏,勾选为红心,不勾选为白心
(1)xml中
<CheckBoxandroid: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" />
(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>
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
}
(2)AppDatabase.kt
@Database(version = 2, entities = [NewStar::class])
abstract class AppDatabase : RoomDatabase() {abstract fun NewStarDao(): NewStarDaocompanion object {private var instance: AppDatabase? = null@Synchronizedfun getDatabase(context: Context): AppDatabase {instance?.let {return it}return Room.databaseBuilder(App.context,AppDatabase::class.java, "app_database").build().apply {instance = this}}}
}
(3)NewStarDao.kt
@Dao
interface NewStarDao {//增@Insertfun 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}
<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=trueLooper.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()}}}}}})
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+频道IDLshouCang.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)}
至此,项目介绍完毕,文中布局和图标等可随需求更改,第一次写博文,手生,写的不好,还望各路大神海涵,如有错误,还请指正。
转载请注明来源,谢谢。
完整代码下载链接:源码
Android-kotlin开发新闻阅读App相关推荐
- AndroidFire,一款新闻阅读 App
AndroidFire 项目地址:AndroidFire 简介:AndroidFire,一款新闻阅读 App,基于 Material Design + MVP + RxJava + Retrofit ...
- c语言什么意思 app 视频 新闻,开发新闻资讯APP需要哪些功能?
原标题:开发新闻资讯APP需要哪些功能? 在过去,人们获取新闻信息的方式一般是通过电视.报纸.广播之类的媒介,而在当今的互联网时代,人们获取信息的途径主要来源于各种新闻资讯APP. 这类APP相较于传 ...
- 开发新闻类APP需要准备什么
不少企业和个人创业者都将其视为一个非常不错的盈利点,想要开发出独立的新闻资讯APP软件,那么开发新闻资讯APP需要哪些功能? 1.海量资讯:栏目频道,包括新闻.娱乐.体育.财经.科技.FM电台.新闻专 ...
- Android Studio实现文艺阅读App
项目目录 一.系统概述 二.系统特点 三.开发环境 四.运行演示 五.源码获取 一.系统概述 本次带来的文艺阅读App可以提供高质量的原创文学作品.用户可以App中找到各种类型的文学作品,包括小说.散 ...
- Apicloud开发新闻类App实战项目-老孟编程
Apicloud开发新闻类App实战项目-老孟编程 课程名称:Apicloud开发新闻类App实战项目 讲师:孟老师 课程介绍: 技术点包括: 1:vue实现apicloud开发脚手架--超级实用通用 ...
- Android kotlin jetpack compose 在APP中部署运行ktor服务器
Android kotlin jetpack compose 在APP中部署运行ktor服务器 前言 添加依赖 服务器管理 活动 效果 DEMO 完事 前言 遇到需求,需要在APP中部署一个服务器,局 ...
- 【Android原生开发】艺术圈APP
项目地址 项目地址github 一个是NodeJS写的服务器(本地),一个是Android端APP 项目背景 艺术来源于生活.以艺术与文化为主体,开发一款APP,主要实现以下五个模块.分别为博物馆模块 ...
- Android studio 开发第一篇 APP项目创建
Android studio开发 APP项目创建 打开Android studio 依次点击file->new->new project 进入create new project界面,选择 ...
- 如何开发新闻阅读器(新闻软件、今日头条)?让我们一起动手吧!
过了几天,博主又匿起来开发了一款新闻阅读器,新闻来源是百度APIStore里的免费API接口,开发的灵感和思路来自今日头条. 有的时候,模范别人应用其实就是一种开发的学习手段,因 ...
最新文章
- 微信小程序万里目_微信小程序学习用推荐:破音万里:音频播放,音乐列表
- 亲身经历揭露淘宝上主板交换的陷阱
- SQL:我为什么慢你心里没数吗?
- Android--Launcher拖拽事件详解【androidICS4.0--Launcher系列二】
- 为什么结构的sizeof不等于每个成员的sizeof之和?
- Date类型之组件方法
- 迅为-iMX6ULL开发板原创嵌入式开发文档系统化学习
- SI4463低功耗测试-STC单片机一样可以超低功耗
- 快速推导出等比数列的求和公式
- php不使用第三变量互换,总结PHP不用第三个变量交换两个变量的值的几种方法
- 如何使用云桌面进行开发?
- 悟空问答 模板 html,WeCenter仿悟空问答模板
- PHP获取自然周日期(周一~周日)
- 课堂笔记 - 数据库设计
- 2013电大计算机综合应用能力实训将邮件保存到考生文件夹,计算机综合应用能力实训指南.doc...
- 【苹果相册】苹果推信群发准入ProvisioningProfile还分为开发和分发
- Android平台简介
- 时序数据库:TimescaleDB的安装
- 根据文件名批量生成文件夹
- 被骗子骗时反把骗子骗!