Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成
玩Android 开放API-玩Android - wanandroid.com
接口使用的是下面的两个:
https://www.wanandroid.com/banner/jsonhttps://www.wanandroid.com/banner/json
wanandroid.com/project/list/1/json?cid=294https://www.wanandroid.com/project/list/1/json?cid=294
需求列表:
现在我需要使用kotlin 来完成下面的需求,使用mvvm的模式,要求使用databinding,viewmodel,livedata,fresco,room,retrofit来完成
1:主页显示https://www.wanandroid.com/banner/json 的数据
使用recyclerView 来展示每一个banner,其中banner内部使用databinding
使用fresco 来加载对应的图片
使用retrofit 来下载数据,下载后要求数据保存在room里面,如果数据在数据库中没有,那么就添加,如果已经有了就更新当前的数据,primarykey 是id
使用viewmode
有一个按钮“刷新banner” 会再次拉取https://www.wanandroid.com/banner/json 来刷新,使用databinding来刷新
2:主页还要显示https://www.wanandroid.com/project/list/1/json?cid=294的数据
使用recyclerView 来展示每一个project,其中project内部使用databinding
使用fresco 来加载对应的图片
使用retrofit 来下载数据,下载后要求数据保存在room里面,如果数据在数据库中没有,那么就添加,如果已经有了就更新当前的数据,primarykey 是id
使用viewmode
有一个按钮“刷新banner” 会再次拉取 https://www.wanandroid.com/project/list/1/json?cid=294 来刷新,使用databinding来刷新
1:https://www.wanandroid.com/banner/json
数据格式如下:
{
"data": [
{
"desc": "我们支持订阅啦~",
"id": 30,
"imagePath": "https://www.wanandroid.com/blogimgs/42da12d8-de56-4439-b40c-eab66c227a4b.png",
"isVisible": 1,
"order": 2,
"title": "我们支持订阅啦~",
"type": 0,
"url": "https://www.wanandroid.com/blog/show/3352"
}
],
"errorCode": 0,
"errorMsg": ""
}
2:https://www.wanandroid.com/project/list/1/json?cid=294
数据格式如下:
{
"data": {
"curPage": 1,
"datas": [
{
"adminAdd": false,
"apkLink": "",
"audit": 1,
"author": "qianyue0317",
"canEdit": false,
"chapterId": 294,
"chapterName": "完整项目",
"collect": false,
"courseId": 13,
"desc": "玩Android flutter版本",
"descMd": "",
"envelopePic": "https://www.wanandroid.com/blogimgs/89868c9a-e793-46f3-a239-751246951b7f.png",
"fresh": false,
"host": "",
"id": 27961,
"isAdminAdd": false,
"link": "https://www.wanandroid.com/blog/show/3618",
"niceDate": "2024-01-29 22:10",
"niceShareDate": "2024-01-29 22:10",
"origin": "",
"prefix": "",
"projectLink": "https://github.com/qianyue0317/wan_android_flutter",
"publishTime": 1706537457000,
"realSuperChapterId": 293,
"selfVisible": 0,
"shareDate": 1706537457000,
"shareUser": "",
"superChapterId": 294,
"superChapterName": "开源项目主Tab",
"tags": [
{
"name": "项目",
"url": "/project/list/1?cid=294"
}
],
"title": "玩Android-flutter项目",
"type": 0,
"userId": -1,
"visible": 1,
"zan": 0
}
],
"offset": 0,
"over": false,
"pageCount": 20,
"size": 15,
"total": 289
},
"errorCode": 0,
"errorMsg": ""
}
0:在应用模块的 build.gradle
文件中,确保启用了数据绑定:
android {
//...
buildFeatures {
dataBinding = true
}
}
1. 依赖项配置
在项目的 build.gradle
文件中添加以下依赖项:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.mykotlin"
compileSdk = 34
defaultConfig {
applicationId = "com.example.mykotlin"
minSdk = 33
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
dataBinding{
enable = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.com.facebook.fresco.fresco4)
implementation(libs.androidx.lifecycle.livedata.ktx) // 这里的版本号可能需要根据实际情况调整
implementation(libs.androidx.lifecycle.viewmodel.ktx) // 同上
implementation(libs.com.squareup.retrofit2.retrofit)
implementation(libs.com.squareup.retrofit2.converter.gson)
//noinspection UseTomlInstead
implementation("androidx.room:room-runtime:2.5.2")
annotationProcessor("androidx.room:room-compiler:2.5.2")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation("androidx.databinding:databinding-runtime:7.4.0")
}
2: 数据模型类
创建数据模型类来表示 Banner 和 Project 的数据:
data class BannerItem(
val id: Int,
val desc: String,
val imagePath: String,
val title: String,
val url: String
)
data class ProjectItem(
val id: Int,
val title: String,
val desc: String,
val envelopePic: String,
// 其他属性
)
3:数据库相关
创建数据库和 DAO:
@Database(entities = [BannerItem::class, ProjectItem::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun bannerDao(): BannerDao
abstract fun projectDao(): ProjectDao
}
@Dao
interface BannerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBanners(banners: List<BannerItem>)
@Query("SELECT * FROM BannerItem")
suspend fun getAllBanners(): List<BannerItem>
}
@Dao
interface ProjectDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertProjects(projects: List<ProjectItem>)
@Query("SELECT * FROM ProjectItem")
suspend fun getAllProjects(): List<ProjectItem>
}
4. Retrofit 服务接口
package com.example.mykotlin.model.data
import android.app.Application
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.example.mykotlin.model.dao.BannerDao
import com.example.mykotlin.model.dao.ProjectDao
import com.example.mykotlin.model.data.network.WanAndroidApi
import com.example.mykotlin.model.db.AppDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class RetrofitSingleton private constructor() {
companion object {
private var instance: retrofit2.Retrofit? = null
fun getInstance(): retrofit2.Retrofit {
if (instance == null) {
synchronized(RetrofitSingleton::class.java) {
if (instance == null) {
try {
val trustAllCerts = arrayOf<TrustManager>(UnsafeTrustManager())
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, SecureRandom())
val client = okhttp3.OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier(UnsafeHostnameVerifier())
.build()
instance = retrofit2.Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(retrofit2.converter.gson.GsonConverterFactory.create())
.client(client)
.build()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: KeyManagementException) {
e.printStackTrace()
}
}
}
}
return instance!!
}
}
}
class UnsafeTrustManager : X509TrustManager {
override fun checkClientTrusted(chain: Array<out java.security.cert.X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out java.security.cert.X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
}
class UnsafeHostnameVerifier : javax.net.ssl.HostnameVerifier {
override fun verify(hostname: String?, sslSession: javax.net.ssl.SSLSession?): Boolean = true
}
class DataRepository(private val application: Application) {
private val TAG = "DataRepository-Kodulf"
// val database = AppDatabase.getInstance(application)
// private val bannerDao: BannerDao = database.bannerDao()
// private val projectDao: ProjectDao = database.projectDao()
val bannerList = MutableLiveData<MutableList<BannerItem>>()
val projectList = MutableLiveData<List<ProjectItem>>()
private val executorService = CoroutineScope(Dispatchers.IO)
// loadBannersFromDatabase()
// loadProjectsFromDatabase()
init {
}
// private fun loadBannersFromDatabase() {
// executorService.launch {
// val banners = bannerDao.getAllBanners()
// if (banners!= null && banners.isNotEmpty()) {
// bannerList.postValue(banners)
// }
// }
// }
// private fun loadProjectsFromDatabase() {
// executorService.launch {
// val projects = projectDao.getAllProjects()
// if (projects!= null && projects.isNotEmpty()) {
// projectList.postValue(projects)
// }
// }
// }
fun fetchBanners(){
val retrofit = Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService = retrofit.create(WanAndroidApi::class.java)
val call = apiService.getBanners()
call.enqueue(object : retrofit2.Callback<BannerResponse> {
override fun onResponse(
call: Call<BannerResponse>,
response: retrofit2.Response<BannerResponse>
) {
if (response.isSuccessful) {
val bannerResponse = response.body()
// 处理返回的数据
Log.d(TAG,"Banner network request fetchBanners " + bannerResponse)
bannerList.postValue(bannerResponse?.data)
}
}
override fun onFailure(call: Call<BannerResponse>, t: Throwable) {
// 处理请求失败的情况
Log.e(TAG,"Banner network request fetchBanners onFailure ",t)
Log.d(TAG,t.toString())
}
})
}
// fun fetchBanners2() {
// Log.d(TAG,"Banner network request fetchBanners")
// val apiService = RetrofitSingleton.getInstance().create(WanAndroidApi::class.java)
//
// runBlocking {
// launch {
// val data = apiService.getBanners().data
//
// Log.d(TAG, "Banner network request fetchBanners data = " + data)
//
// data?.let {
// Log.d(TAG, "Banner network request fetchBanners doBannerUpdate")
//
doBannerUpdate(data)
// try {
bannerDao.insertBanners(bannerItems)
// // 更新 LiveData 的值
// Log.d(
// TAG,
// "Banner network request fetchBanners doBannerUpdate bannerList.postValue(bannerItems)"
// )
//
// bannerList.postValue(data)
// } catch (e: Exception) {
// println("Error updating or inserting banners: ${e.message}")
// }
// }
// }
// }
// }
private fun doBannerUpdate(bannerItems: MutableList<BannerItem>) {
executorService.launch {
try {
// bannerDao.insertBanners(bannerItems)
// 更新 LiveData 的值
Log.d(TAG,"Banner network request fetchBanners doBannerUpdate bannerList.postValue(bannerItems)")
bannerList.postValue(bannerItems)
} catch (e: Exception) {
println("Error updating or inserting banners: ${e.message}")
}
}
}
//
// fun fetchProjects() {
// val apiService = RetrofitSingleton.getInstance().create(WanAndroidApi::class.java)
// executorService.launch {
// val data = apiService.getProjects().data;
// data?.let{
// doProjectUpdate(data)
// }
// }
// }
//
// private fun doProjectUpdate(datas: List<ProjectItem>) {
// executorService.launch {
// try {
// projectDao.insertProjects(datas)
// // 更新 LiveData 的值
// projectList.postValue(datas)
// } catch (e: Exception) {
// println("Error updating or inserting projects: ${e.message}")
// }
// }
// }
}
5:ViewModel
package com.example.mykotlin.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.mykotlin.model.data.BannerItem
import com.example.mykotlin.model.data.DataRepository
//注意这里application 前面没有var
class HomeViewModel(application: Application) : AndroidViewModel(application) {
init {
}
private val dataRepository:DataRepository = DataRepository(application)
private val bannerList:MutableLiveData<MutableList<BannerItem>> = dataRepository.bannerList
fun refreshBanner(){
dataRepository.fetchBanners();
}
fun getBannerList():MutableLiveData<MutableList<BannerItem>>{
return bannerList
}
}
6. 布局文件
创建主布局文件 activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.example.mykotlin.viewmodel.HomeViewModel"/>
<variable
name="viewModel"
type="HomeViewModel" />
</data>
<LinearLayout 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"
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/jumpXieCheng"
android:onClick="jumpXieCheng"
android:text="跳转到协程" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/refreshBannerButton"
android:onClick="refreshBanner"
android:text="刷新banner" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/refreshProjectButton"
android:onClick="refreshProject"
android:text="刷新project" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="#990000"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bannerRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="#009900"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/projectRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
创建 Banner 项布局文件 banner_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.example.mykotlin.model.data.BannerItem"/>
<variable
name="banner"
type="BannerItem" />
</data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="200dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/des"
android:text="123"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title"
android:text="1234"/>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/black"/>
</LinearLayout>
</layout>
创建 Project 项布局文件 project_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="project"
type="com.example.yourpackage.ProjectItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/projectImage"
android:layout_width="match_parent"
android:layout_height="150dp"
app:srcCompat="@{project.envelopePic}" />
<TextView
android:id="@+id/projectTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{project.title}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/projectImage" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
7. Activity
package com.example.mykotlin.view
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.mykotlin.R
import com.example.mykotlin.databinding.ActivityMainBinding
import com.example.mykotlin.model.data.BannerItem
import com.example.mykotlin.viewmodel.HomeViewModel
import com.example.mykotlin.xiecheng.XieChengActivity
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding:ActivityMainBinding
lateinit var homeViewModel:HomeViewModel
//初始化为空的列表
var list:MutableList<BannerItem> = mutableListOf()
var bannerAdapter: BannerAdapter = BannerAdapter(list)
var TAG:String = "kodulf-Mainactivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
homeViewModel = ViewModelProvider(
viewModelStore,//这个对象只有在Activity创建之和才会有
defaultViewModelProviderFactory
).get(HomeViewModel::class.java)
homeViewModel.getBannerList().observe(this, Observer { value ->
Log.d(TAG, "getBannerListLiveData onchanged + $value")
bannerAdapter.setBanners(value)
})
activityMainBinding.bannerRecyclerView.adapter = bannerAdapter
activityMainBinding.bannerRecyclerView.layoutManager = LinearLayoutManager(this)
}
fun jumpXieCheng(view: View) {
val intent = Intent(this,XieChengActivity::class.java)
startActivity(intent)
}
fun refreshBanner(view:View){
homeViewModel.refreshBanner()
}
}
8. 适配器
创建 Banner 适配器和 Project 适配器:
package com.example.mykotlin.view
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.mykotlin.R
import com.example.mykotlin.databinding.ItemBannerBinding
import com.example.mykotlin.model.data.BannerItem
class BannerAdapter(private val banners: MutableList<BannerItem>) : RecyclerView.Adapter<BannerAdapter.BannerViewHolder>() {
class BannerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(bannerItem: BannerItem) {
// 绑定数据到视图的逻辑
itemView.findViewById<TextView>(R.id.des).setText(bannerItem.desc)
itemView.findViewById<TextView>(R.id.title).setText(bannerItem.title)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BannerViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_banner, parent, false)
return BannerViewHolder(view)
}
override fun onBindViewHolder(holder: BannerViewHolder, position: Int) {
holder.bind(banners[position])
}
override fun getItemCount(): Int {
return banners.size
}
fun setBanners(newBanners: MutableList<BannerItem>) {
banners.clear()
banners.addAll(newBanners)
notifyDataSetChanged()
}
}