思考怎么去做一个网络api的调取, 然后展示到android前端
需求分析
- 考虑几个页面,页面展示什么内容
- 我需不需要更改后端内容, 如果只是单纯的获取数据,那就只需考虑
GET data
- 如果我需要更改后端数据库的内容, 那么什么参数和什么事件将会导致我这样做
- 考虑怎么配置网络请求, 网络请求返回的数据要不要存入到数据库
- 如果要存入到数据库,那么数据项目的配置是怎么样的
- 没有网络的情况下还能不能直接去拿到已经存在数据库的内容
- 考虑适配的android平台, 这里以android 13 api 33 为例
实现一个从网络请求获取todo数据,展示在app上面
先任务简单化(注意使用到viewbinding,可能部分代码需要配合xml文件名具体分析)
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.drakeet.multitype:multitype:4.3.0'
def activity_version = "1.9.2"
implementation "androidx.activity:activity-ktx:$activity_version"
def room_version = "2.4.2"
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-runtime:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation 'com.github.alhazmy13:Catcho:v1.1.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.9'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id 'com.google.devtools.ksp'
}
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
id 'com.google.devtools.ksp' version '1.9.24-1.0.20' apply false
}
1.配置BASE_URL
- private const val BASE_URL = “https://7ce074a2.r7.cpolar.top”
2. 配置好mongodb connect string后, 启动后端
3. 找到需要的接口
- GET api/todos 是我需要的
- 接口返回示例
HTTP/1.1 200 OK
Date: Fri, 21 Feb 2025 07: 58: 10 GMT
Content-Type: application/json
Content-Length: 418
Vary: Origin
Connection: close
[
{
"_id": "67b8244a6d9be942d1ba3a8a",
"completed": true,
"body": "hello world"
},
{
"_id": "67b825616d9be942d1ba3a8b",
"completed": true,
"body": "Damn"
},
{
"_id": "67b828926d9be942d1ba3a8c",
"completed": true,
"body": "哈哈"
},
{
"_id": "67b829796d9be942d1ba3a8d",
"completed": true,
"body": "AAA"
},
{
"_id": "67b829896d9be942d1ba3a8e",
"completed": true,
"body": "你好呀"
},
{
"_id": "67b82da36d9be942d1ba3a8f",
"completed": true,
"body": "及你太美"
}
]
4. 编写接口
interface TodoApi {
@GET("api/todos")
suspend fun getAllTodos(): Response<List<Todo>>
}
5. 编写retrofit客户端
object ApiService {
private const val BASE_URL = "https://7ce074a2.r7.cpolar.top"
val todoApi: TodoApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TodoApi::class.java)
}
}
6. 数据库内容项保存
@Entity(
tableName = "todos",
)
data class Todo(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val _id: String = "",
val completed: Boolean,
val body: String,
)
7.dao查询接口
@Dao
interface TodoDao {
@Query("SELECT * FROM todos")
suspend fun getAllTodos(): List<Todo>
}
8. 数据库需要
@Database(
entities = [
Todo::class
],
version = 1,
exportSchema = false
)
abstract class TodoDB : RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object {
@Volatile
private var instance: TodoDB? = null
fun getDatabase(context: Context): TodoDB {
return instance ?: Room.databaseBuilder(
context.applicationContext,
TodoDB::class.java,
"todo_database"
).build()
instance = instance
instance
}
}
}
9. viewmodel层怎么搞,我也不会
class TodoViewModel(
private val api: TodoApi,
private val db: TodoDB
) : ViewModel() {
fun getAllTodos() {
viewModelScope.launch {
val response = api.getAllTodos()
Log.d("GET返回", response.body().toString())
if (response.isSuccessful && response.body() != null) {
Log.d("GET响应", response.body().toString())
response.body()!!.forEach { it ->
db.todoDao().insertTodo(it)
}
}
}
}
}
10. 造一个activity和xml文件
- TodoActivity.kt
- activity_todo.xml: 需要刷新layout,和recyclerview
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/swiperefreshlayout"
android:layout_width="match_parent" android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
- item_todo.xml: recyclerview里面的数据项
<?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="wrap_content"
android:orientation="vertical" android:padding="16dp">
<TextView android:id="@+id/tvTitle" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="Todo Title" android:textSize="18sp" />
<TextView android:id="@+id/tvCompleted" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="Completed" android:textSize="14sp" />
</LinearLayout>
11. 为recyclerview编写一个adapter,这里使用MultiType库
class TodoItemViewBinder : ItemViewBinder<Todo, TodoItemViewBinder.ViewHolder>() {
inner class ViewHolder(
private val binding: ItemTodoBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(todo: Todo) {
binding.tvTitle.text = todo.body
binding.tvCompleted.text = if (todo.completed) "已完成" else "暂时没搞定"
}
}
override fun onBindViewHolder(holder: ViewHolder, item: Todo) {
holder.bind(item)
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
val binding = ItemTodoBinding.inflate(inflater, parent, false)
return ViewHolder(binding)
}
}
12. 来实操TodoActivity.kt, 始终应该继承app compat activity,主要看onCreate内容就行
class TodoActivity : AppCompatActivity() {
private var multitypeAdapter = MultiTypeAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTodoBinding.inflate(layoutInflater)
if (!com.example.myapplication.BuildConfig.IS_DEBUG) {
setUpCatcho(this)
}
setContentView(binding.root)
loadTodos()
binding.swiperefreshlayout.setOnRefreshListener {
loadTodos()
}
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = multitypeAdapter
multitypeAdapter.register(Todo::class.java, TodoItemViewBinder())
}
}
13.抽离出loadTodos,设置一个私有函数(没调试成功)
private fun loadTodos() {
MainScope().launch {
try {
if (NetworkCheckConnUtil.isNetworkAvailable(this@TodoActivity)) {
val response = ApiService.todoApi.getAllTodos()
if (response.isSuccessful && response.body() != null) {
val todos = response.body()!!
todos.forEach { it ->
todoDB?.todoDao()
?.insertTodo(it)
}
multitypeAdapter.items = todos
multitypeAdapter.notifyDataSetChanged()
binding.recyclerView.adapter = multitypeAdapter
}
} else {
Toast.makeText(this@TodoActivity, "网络不可用", Toast.LENGTH_SHORT)
.show()
loadTodosFromDatabase()
}
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this@TodoActivity, "网络不可用", Toast.LENGTH_SHORT)
.show()
loadTodosFromDatabase()
} finally {
binding.swiperefreshlayout.isRefreshing = false
}
}
}
遇到的问题(以上内容有修改了,看源码为主)
1. retrofit无网络,或者base url配置错时, 程序会闪退,这个东西要额外在viewmodel层处理
- ok,解决bug的逻辑是这样,一开始就加载本地数据库内容(然后土司网络不可用),知道下滑刷新才加载网络内容,how
about that - 程序一开始就没网的时候就闪退,我解决不了
总结
- 实现刷新加载网络数据成功
- 但是没有实现从数据库中获取内容,调试不通,无网络时,会报错误
java.net.UnknownHostException: Unable to resolve host "7ce074a2.r7.cpolar.top": No address associated with hostname
at
效果截图

ref link:
- https://gitee.com/EEPPEE_admin/android-mono-repo/tree/aaa/app/src/main/java/com/example/myapplication/todoapp
- 后端: https://gitee.com/EEPPEE_admin/probe-examples/tree/master/fiber-and-react-example/backend