跳到正文 Seminar Week 8 Summary · Jimmy Huang's Blog
← Week 8 Network Connection in Android and Retrofit 学习 › C6001 › 2026 S1 › FIT5046 Mobile and distributed computing systems › Week 8 Network Connection in Android and Retrofit Seminar Week 8 Summary Seminar Week 8 Summary 本页目录· 56 条 本周 Seminar 主要讲 Android 应用如何通过网络访问外部数据源,重点是使用 Retrofit 在 Android / Kotlin 项目中调用第三方 RESTful API,并把网络返回的 JSON 数据转换成应用内部可以使用的 Kotlin 对象。
可以这样理解:
上周的 Room 解决的是“数据存在本地”的问题。
本周的 Retrofit 解决的是“数据来自网络 API”的问题。
Repository 和 ViewModel 是把这两种数据源整合到 Android 架构里的关键中间层。
Learning Outcomes 理解公开网络 API(Public Web API)是什么,以及移动端如何通过 HTTP 请求访问远程服务。
理解 Web Services、SOA、SOAP 和 RESTful Web Services 之间的关系。
区分 REST 是一种架构风格,不是一个单独的网络协议。
理解 RESTful API 中的资源(Resource)、URI / URL、请求方法和响应表示(Representation)。
掌握 JSON 的基本结构,并理解 JSON 如何映射到 Kotlin data class。
知道 Android 应用访问网络时需要配置 Manifest 权限和 Gradle 依赖。
掌握 Retrofit 的核心组成:Model Class、Interface、Object、Repository、ViewModel 和 Composable UI。
理解为什么网络请求要使用 suspend function、coroutine 和 viewModelScope.launch。
理解课堂中 Google Custom Search 示例与实际 Lab 更新示例之间的关系。
本周在课程中的位置 本周内容承接前一周的 Room 本地数据库,并开始进入“网络数据源”的部分。
本页目录 周次 / 主题 数据来源 主要工具 在架构中的作用 Room / Local Storage 本地设备数据库 Room 在本地持久化保存数据 Retrofit / Network API 远程服务器 API Retrofit 从网络获取 JSON 数据 Repository 本地 + 网络 Repository class 抽象不同数据源,统一提供数据访问入口 ViewModel Repository 输出的数据 ViewModel 保存 UI 状态,并把数据暴露给 Compose UI
老师课堂中特别强调:Room 和 Retrofit 不是孤立的两个技术点。它们在 Android 架构中都应该通过 Repository 被封装起来,再由 ViewModel 暴露给 UI。也就是说,Repository 是连接本地数据库和网络 API 的关键中间层。
1. Public Web APIs:公开网络 API 公开网络 API(Public Web API / Open Web API)是指服务提供方通过互联网暴露出来的接口,让第三方客户端可以访问它们的业务逻辑、数据或服务。
例如,一个天气 App 可以向 OpenWeatherMap API 发起请求:
手机 App、网页浏览器或其他客户端发起 HTTP GET 请求。
请求到达 API 层。
API 服务访问服务器和数据库。
服务器把结果封装成 JSON 响应返回给客户端。
客户端解析 JSON,并在 UI 中显示结果。
可以这样理解:API 是客户端和服务器之间的“约定好的入口”。客户端不需要知道服务器内部数据库和业务逻辑怎么写,只要按照 API 文档发送正确的请求即可。
2. Web Services 与 SOA 的背景 公开 Web API 的发展来源于 Web Services 和 面向服务架构(Service-Oriented Architecture, SOA)。
2.1 SOA 的核心思想 面向服务架构强调把应用功能拆成可以复用的服务,并通过标准接口暴露给外部系统。
可以这样理解:SOA 关心的是“把功能包装成服务,并让不同系统能够通过统一接口调用这些服务”。
2.2 Web Services 的定义 Web Service 是一种支持网络上机器与机器之间交互的软件或代码。它可以提供:
业务逻辑(Business Logic)
数据(Data)
流程(Processes)
其他服务(Other Services)
Web Services 最初常用 SOAP 实现,后来逐渐演变到 RESTful Web Services。
3. SOAP Web Services SOAP(Simple Object Access Protocol)是一种较早的 Web Service 实现方式。
3.1 SOAP 的特点
3.2 SOAP 请求结构 课件中的 SOAP 示例展示了一个 XML 格式的请求:
课堂补充中提到,SOAP 相对更重、更规则化,并且高度依赖 XML。虽然 Android 也可以通过其他库处理 SOAP,但本周 Retrofit 主要用于消费 RESTful API,不是 SOAP API。
4. RESTful Web Services RESTful Web Services 是基于 REST 架构风格(REST Architecture)实现的 Web Services。
REST 由 Roy Fielding 在博士论文中提出。需要注意的是:
REST 不是一个具体协议。
REST 不是一个单独标准。
REST 是一种架构风格,它定义了一组设计约束。
HTTP
URL / URI
JSON / XML / HTML
MIME Types / Media Types,例如: application/json
text/xml
image/gif
audio/mpeg
可以这样理解:HTTP 是传输协议,JSON 是常见数据格式,REST 是组织这些请求和资源的架构风格。
5. REST 的架构约束 课堂中特别强调:RESTful API 的关键思想是“把一切都看成资源(Resource)”,每个资源都可以通过网络路径被定位和访问。
6. Resource、URI、URL 与 Endpoint
6.1 什么是 Resource 资源(Resource)指任何可以被命名的信息。例如:
一个 customer
一本 book
一个 student
一篇 post
一条 comment
在 RESTful API 中,每个资源都应该可以通过资源标识符(Resource Identifier)访问。
6.2 URI / URL 的作用 URI(Uniform Resource Identifier)强调“唯一标识某个资源”。
URL(Uniform Resource Locator)强调“资源所在的位置”。
在日常 API 开发中,URL 经常被直接用来访问资源,例如:
它表示访问 posts 资源集合中 ID 为 3 的单条记录。
6.3 Query Parameters 查询参数(Query Parameters)一般放在 ? 后面,用于过滤、搜索或补充请求条件。
? 后面开始写参数。
每个参数是 key=value。
多个参数之间用 & 连接。
课堂中提到,Assignment 4 可能会需要组合多个查询条件作为过滤逻辑,因此要理解 URL 参数的结构。
7. HTTP Request and Response RESTful API 通常通过 HTTP 请求和响应进行交互。
7.1 常见 HTTP 方法 本周示例主要使用 GET,因为它适合演示“从远程 API 读取 JSON 数据”。但老师也提醒,后续 Assignment 可能需要根据业务逻辑使用 POST,把数据写回服务端。
7.2 Request 与 Response 的关系 请求(Request)需要什么: Base URL 是什么?
Endpoint 是什么?
需要哪些 path parameter?
需要哪些 query parameter?
是否需要 API key?
响应(Response)会返回什么: 返回 JSON 还是 XML?
最外层是 object 还是 array?
字段名是什么?
字段类型是什么?
哪些字段是 App 真正需要的?
这一步很重要,因为 Retrofit 的 data class、interface 返回类型和 Gson 转换都依赖响应结构。
8. Representational State Transfer 的含义 REST 是 Representational State Transfer 的缩写。
8.1 Representation 是什么 表示(Representation)可以理解为“某个资源当前状态的文档形式”。
8.2 State Transfer 是什么 状态转移(State Transfer)指客户端和服务器通过资源的表示来传递资源状态。
可以这样理解:REST 不是让客户端直接操作数据库,而是让客户端通过 HTTP 请求操作“资源”,然后服务器把资源状态用 JSON / XML 等表示形式返回。
9. JSON 基础 JSON(JavaScript Object Notation)是一种轻量级文本数据交换格式。
轻量、可读性较好。
使用 key-value 结构表达数据。
对机器解析友好,适合网络传输。
常用于 RESTful API 的请求和响应。
9.1 JSON 的两种核心结构
9.2 JSON value 的类型 string
number
true / false
null
object
array
课堂中老师提醒:JSON 类型不匹配通常不是编译时报错,而是网络请求返回后,Gson 转换时才出现运行时错误。因此调试时要仔细检查 JSON 原始响应。
9.3 JSON 在线解析工具 这些工具适合在调试时使用,用来把一长串 JSON 格式化,检查字段名、层级、数组和对象结构。
10. Android 中的 Network Connection 大多数联网 Android App 会使用 HTTP 与远程数据源通信。
课件列出的 Android HTTP 调用方式包括:
本周重点是 Retrofit,因为它更适合把 HTTP API 映射成 Kotlin interface,代码结构清晰,也方便和 Gson、coroutine、ViewModel 结合。
11. Retrofit 是什么 Retrofit 是 Square 开发的一个 Android / Java HTTP client library,用来简化对 RESTful API 的访问。
把 HTTP API 声明成 Kotlin / Java interface。
使用注解(Annotations)描述请求方法、endpoint 和参数。
基于 OkHttp 执行底层网络请求。
通过 converter,例如 Gson,把 JSON 与 Kotlin / Java 对象相互转换。
可以这样理解:Retrofit 帮你把“手写 HTTP 请求 + 手动解析 JSON”变成“定义接口 + 定义数据类 + 调用函数”。
12. 使用 Retrofit 前的项目配置
12.1 Manifest 网络权限 Android App 如果要访问互联网,必须在 AndroidManifest.xml 中加入网络权限。
位置:放在 <manifest> 内部、 <application> 之前。
老师课堂中提醒:这行权限可能会在 quiz 中考到。常见错误是忘记加,或者加错位置。
12.2 Gradle Dependencies 需要在 module-level Gradle file 中加入 Retrofit 和 Gson 相关依赖。
在使用 Version Catalog 的项目中,Android Studio 可能会把依赖转换成类似:
13. Retrofit 的整体开发流程 Retrofit Model Classes
Retrofit Interface
Retrofit Object
14. 第一步:先理解 API Request 与 Response 这一步决定后面 data class 怎么写,也决定 Retrofit Interface 的返回类型是什么。
15. 第二步:Retrofit Model Class Retrofit Model Class 的作用是把 JSON response body 映射成 Kotlin 对象。
课件中的 Google Custom Search 示例只关心 JSON 响应里 items 下的 snippet 字段,所以定义了两个 data class:
关键点:data class 不一定要把 JSON 中所有字段都写出来,只需要写 App 真正要用的字段。但字段名和类型要能正确对应 JSON 结构。
课堂中老师用 JSONPlaceholder 的 posts 示例补充说明:如果返回的 post 有四个字段,例如:
那么对应的 Kotlin data class 可以是:
16. 第三步:Retrofit Interface Retrofit Interface 用来声明 HTTP 请求。
课件中的 Google Custom Search 示例:
@GET("customsearch/v1"):请求方法是 GET,endpoint 是 customsearch/v1。
@Query("key"):URL query parameter 名字是 key。
@Query("cx"):URL query parameter 名字是 cx。
@Query("q"):URL query parameter 名字是 q。
suspend fun:这是一个可能耗时的网络请求,必须放在 coroutine 中调用。
SearchResponse:返回的数据会被 Gson 转换成 SearchResponse 对象。
16.1 @Query 与 @Path 的区别
16.2 Interface 中哪些能改,哪些不能改 课堂中老师强调:第三方 API 定义好的 endpoint 和参数名必须遵守;你能控制的是本地函数名、data class 设计和如何把返回数据映射进应用。
17. 第四步:API HTTP Call and Parameters 在 RetrofitObject 中声明 Base URL。
在 RetrofitInterface 中补全 endpoint 和 query parameters。
18. 第五步:Retrofit Object Retrofit Object 用来创建 Retrofit service instance。
18.1 Retrofit.Builder 做了什么
18.2 为什么用 object 和 lazy 可以这样理解:Retrofit service 没必要每次请求都重新创建。用 singleton 可以避免重复创建对象;用 lazy 可以避免程序启动时就做不必要的初始化。
老师课堂中提到,这一部分可能是整段 Retrofit 代码里最难理解的部分,尤其是 singleton、lazy initialization 和线程安全的概念。
19. 第六步:Retrofit and Repository Repository class 用来处理数据操作,并隐藏具体的 Retrofit 调用细节。
持有 Retrofit service。
封装 API key、search ID 等本地配置。
提供一个更高层的数据访问函数,例如 getResponse()。
让 ViewModel 不需要关心 Retrofit 的底层细节。
未来可以同时协调 Room 本地数据库和 Retrofit 网络请求。
课堂中老师强调:在 Assignment 中,Repository 应该作为不同数据源之间的中间人。它可以负责本地 Room 数据,也可以负责远程 Retrofit 数据。
20. 第七步:Retrofit, Repository and ViewModel ViewModel 负责调用 Repository,并把结果保存成 UI 可以观察的状态。
viewModelScope.launch:启动 coroutine 来执行网络请求。
Repository 中的函数是 suspend,所以必须在 coroutine 中调用。
mutableStateOf():保存网络请求结果,供 Compose UI 观察。
try-catch:捕获网络错误,避免异常直接暴露给用户界面。
ViewModel 会在 recomposition 和 configuration changes 中保留数据,因此保存网络响应时不需要在 UI 中再用 remember。
注意:课件中 Repository 函数名有 getResponse(),ViewModel 示例中调用了 repository.customSearch(keyword),实际代码中应保持函数命名一致。理解重点是:ViewModel 调用 Repository 的 suspending function,并把结果写入 state。
21. 第八步:Composable and Retrofit Compose UI 负责收集用户输入、触发 ViewModel 函数,并根据 state 更新界面。
21.1 UI 更新流程 用户在 OutlinedTextField 输入关键词。
keyword 用 remember { mutableStateOf("") } 保存,因为这是 UI 层的本地输入状态。
用户点击 Button。
Button 调用 viewModel.customSearch(keyword)。
ViewModel 内部用 viewModelScope.launch 启动 coroutine。
ViewModel 调用 Repository 的 suspending function。
Repository 通过 Retrofit service 发起 HTTP 请求。
Retrofit 收到 JSON response,并用 Gson 转成 Kotlin 对象。
ViewModel 更新 retrofitResponse。
Compose 观察到 state 改变,自动 recomposition。
UI 显示新的 snippet 结果。
21.2 remember 与 ViewModel 的区别 可以这样理解:输入框文字是当前界面的临时状态;网络返回结果是更重要的屏幕状态,应该由 ViewModel 管理。
22. Google Custom Search 示例与实际 Lab 更新 课件中的主要代码示例使用 Google Custom Search API:
但课堂中老师补充:Google Custom Search 对新用户的 API key / Search ID 获取方式发生变化,新用户可能无法继续按旧方式免费获取 key。因此:
课件中的 Google Custom Search 代码仍然适合 quiz 和概念学习。
但实际本周 Lab / 课堂运行代码会使用替代的免费 RESTful API 示例。
老师提到会使用 JSONPlaceholder 这类不需要 API key 的服务作为练习示例。
JSONPlaceholder 的例子更简单,例如:
先看 response JSON。
定义 Post data class。
在 interface 中声明 endpoint 和 query / path 参数。
在 object 中配置 base URL。
通过 repository 调用 service。
ViewModel 用 coroutine 发起请求并更新 state。
Compose UI 展示结果。
23. Retrofit 开发中的常见错误
23.1 忘记网络权限 如果没有在 AndroidManifest.xml 中加入:
23.2 Base URL、Endpoint 和 Query 参数混淆
23.3 data class 与 JSON 类型不匹配 就可能导致 Gson 转换出现问题。调试时要检查 JSON 原始结构和字段类型。
23.4 忘记 suspend 和 coroutine 网络请求不可预测,也可能耗时,不能直接阻塞主线程。Retrofit interface 中的网络函数应使用 suspend:
23.5 没有处理异常 网络断开。
API 地址写错。
参数错误。
JSON 转换失败。
服务器暂时不可用。
因此 ViewModel 中应使用 try-catch:
课堂中老师提醒:错误不应直接暴露给用户界面。调试时可以在 Logcat 中查看错误日志,并确认当前运行的是正确的 emulator / device。
23.6 不理解 Repository 的作用 不要让 Composable UI 直接处理所有 Retrofit 细节。更好的结构是:
Repository 的价值在于隐藏数据来源。以后数据可能来自 Room、本地文件、缓存或远程 API,UI 不需要知道这些细节。
24. 本周核心代码结构总览 可以用下面的结构记忆 Retrofit 的完整工作方式:
25. 和 Assignment / Lab 的关联 本周内容对后续 Lab 和 Assignment 很重要,原因是:
真实移动 App 通常不会只依赖本地数据,也会从服务器获取数据。
Assignment 可能需要根据用户输入组合查询条件,对远程数据进行过滤或搜索。
如果 App 需要写入远程服务,可能不只用 GET,也可能涉及 POST。
Repository 架构有助于同时管理本地 Room 数据和远程 Retrofit 数据。
Retrofit 的接口定义、data class 映射和 ViewModel coroutine 调用,是后续实现网络功能的基础。
本周 Moodle 上提供的 source files 需要自己创建项目后导入,并修复引用问题。
Google Custom Search 示例用于理解概念和 quiz;实际练习应按更新后的 Lab notes 使用替代 API。
如果未来 API key 必须使用,示例中可以先硬编码,但更好的实践是不要把敏感 key 直接暴露在公开代码库中。
Summary 本周 Seminar 的核心是:Android App 如何通过 Retrofit 访问 RESTful API,并把远程 JSON 数据纳入 Compose + ViewModel + Repository 的架构中。
本周需要真正掌握的不是单独某一行代码,而是整个数据流:从用户点击按钮,到 ViewModel 发起 coroutine,再到 Repository 调用 Retrofit,最后 JSON 被转换成 Kotlin 对象并显示在 UI 上。
https://api.openweathermap.org/data/2.5/weather?lat=57&lon=-2.15&appid={API key}&units=metrichttps://jsonplaceholder.typicode.com/posts/3https://example.com/search?city=Melbourne&format=jsonhttp://json.parser.online.fr/
https://jsoneditoronline.org/Request and its parameters
AND
Responsehttps://www.googleapis.com/customsearch/v1?key=API_KEY&cx=SEARCH_ID_cx&q=keywordhttps://jsonplaceholder.typicode.com/posts/3https://www.googleapis.com/customsearch/v1?key=API_KEY&cx=SEARCH_ID_cx&q=keywordhttps://www.googleapis.com/customsearch/v1?key=API_KEY&cx=SEARCH_ID_cx&q=keywordhttps://jsonplaceholder.typicode.com/posts
https://jsonplaceholder.typicode.com/posts/3
https://jsonplaceholder.typicode.com/commentsBase URL: https://www.googleapis.com/
Endpoint: customsearch/v1
Query: ?key=API_KEY&cx=SEARCH_ID_cx&q=keywordComposable UI
↓
ViewModel
↓
Repository
↓
Retrofit Service
↓
Remote API1. AndroidManifest.xml
- Add INTERNET permission
2. build.gradle.kts
- Add Retrofit
- Add converter-gson
- Add Gson
3. Model data classes
- Define Kotlin classes matching JSON response
4. RetrofitInterface
- Declare @GET / @POST / @PUT / @DELETE
- Define @Query or @Path parameters
- Return model type
- Use suspend function
5. RetrofitObject
- Define BASE_URL
- Build Retrofit instance
- Add GsonConverterFactory
- Create service from interface
- Use singleton + lazy
6. Repository
- Hold service instance
- Provide suspending data functions
- Hide API details from ViewModel
7. ViewModel
- Launch coroutine
- Call repository
- Store result in mutableStateOf
- Handle exceptions
8. Composable UI
- Store local input state with remember
- Call ViewModel function on button click
- Observe ViewModel state
- Recompose when data changesRemote API returns JSON
↓
Retrofit Interface declares HTTP request
↓
Gson converts JSON to Kotlin data class
↓
RetrofitObject provides singleton service
↓
Repository abstracts data operation
↓
ViewModel launches coroutine and stores state
↓
Composable observes state and recomposes UI<? xml version= " 1.0 " ?> < soap : Envelope xmlns:soap= " http://www.w3.org/2001/12/soap-envelope " soap:encodingStyle= " http://www.w3.org/2001/12/soap-encoding " > < soap : Body > < m : GetPrice xmlns:m= " http://www.w3schools.com/prices " > < m : Item >Apples</ m : Item > </ m : GetPrice > </ soap : Body > </ soap : Envelope > { " phoneNumber " : [ { " type " : " home " , " number " : " 212 555-1234 " }, { " type " : " fax " , " number " : " 646 555-4567 " } ] } 最外层 Object └── " phoneNumber " : Array ├── 第 1 个电话号码 Object │ ├── " type " : " home " │ └── " number " : " 212 555-1234 " │ └── 第 2 个电话号码 Object ├── " type " : " fax " └── " number " : " 646 555-4567 " { " firstName " : " John " , " lastName " : " Smith " , " age " : 25, " address " : { " streetAddress " : " 21 2nd Street " , " city " : " New York " , " state " : " NY " , " postalCode " : 10021 } } < uses-permission android:name= " android.permission.INTERNET " /> <? xml version= " 1.0 " encoding= " utf-8 " ?> < manifest xmlns:android= " http://schemas.android.com/apk/res/android " xmlns:tools= " http://schemas.android.com/tools " > < uses-permission android:name= " android.permission.INTERNET " /> < application ...> </ application > </ manifest > implementation( "com.squareup.retrofit2:retrofit:3.0.0" ) implementation( "com.squareup.retrofit2:converter-gson:3.0.0" ) implementation( "com.google.code.gson:gson:2.13.2" ) implementation(libs.retrofit) implementation(libs.converter.gson) implementation(libs.gson) data class SearchResponse( val items: List<Items> = ArrayList() ) data class Items( val snippet: String ) { " userId " : 1, " id " : 1, " title " : " example title " , " body " : " example body " } data class Post( val userId: Int, val id: Int, val title: String, val body: String ) interface RetrofitInterface { @GET( "customsearch/v1" ) suspend fun customSearch( @Query( "key" ) API_KEY: String, @Query( "cx" ) SEARCH_ID_cx: String, @Query( "q" ) keyword: String ): SearchResponse } @GET( "posts/{id}" ) suspend fun getPostById( @Path( "id" ) id: Int ): Post private val BASE_URL = "https://www.googleapis.com/" interface RetrofitInterface { @GET( "customsearch/v1" ) suspend fun customSearch( @Query( "key" ) API_KEY: String, @Query( "cx" ) SEARCH_ID_cx: String, @Query( "q" ) keyword: String ): SearchResponse } object RetrofitObject { private val BASE_URL = "https://www.googleapis.com/" val retrofitService: RetrofitInterface by lazy { Log.i( "RetrofitObject" , "called" ) val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() retrofit.create(RetrofitInterface::class.java) } } class ItemsRepository { private val searchService: RetrofitInterface = RetrofitObject.retrofitService private val API_KEY = "" private val SEARCH_ID_cx = "" suspend fun getResponse(keyword: String): SearchResponse { return searchService.customSearch( API_KEY, SEARCH_ID_cx, keyword ) } } class RetrofitViewModel : ViewModel() { private val repository = ItemsRepository() val retrofitResponse = mutableStateOf(SearchResponse()) fun customSearch(keyword: String) { viewModelScope.launch { try { retrofitResponse. value = repository.customSearch(keyword) } catch (e: Exception) { Log.i( "Error" , "Response failed" , e) } } } } @Composable fun SearchResults(viewModel: RetrofitViewModel) { val itemsReturned by viewModel.retrofitResponse var keyword by remember { mutableStateOf( "" ) } Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = keyword, onValueChange = { keyword = it }, label = { Text( "Keyword" ) }, modifier = Modifier .fillMaxWidth() .padding(bottom = 8.dp) ) Button( onClick = { viewModel.customSearch(keyword) }, modifier = Modifier .fillMaxWidth() .height(48.dp) ) { Text( "Search" ) } if (itemsReturned.items.isNotEmpty()) { val resultSnippet = itemsReturned.items[0].snippet Text( text = "Results:$resultSnippet" , modifier = Modifier.padding(top = 8.dp) ) } } } 用户在输入框输入 keyword ↓ keyword 状态更新 ↓ 用户点击 Search 按钮 ↓ 调用 viewModel.customSearch(keyword) ↓ ViewModel 调用 Repository ↓ Repository 调用 Retrofit API ↓ 服务器返回 JSON ↓ Gson 转成 SearchResponse ↓ ViewModel 更新 retrofitResponse.value ↓ Composable 里的 itemsReturned 观察到变化 ↓ UI 自动重新刷新 ↓ 显示第一个 snippet { " userId " : 1, " id " : 1, " title " : " example title " , " body " : " example body " } < uses-permission android:name= " android.permission.INTERNET " /> private val BASE_URL = "https://www.googleapis.com/" @GET( "customsearch/v1" ) suspend fun customSearch(...) data class Post( val id: String ) @GET( "posts" ) suspend fun getPosts(): List<Post> viewModelScope.launch { // call suspend function here } viewModelScope.launch { try { retrofitResponse. value = repository.getResponse(keyword) } catch (e: Exception) { Log.i( "Error" , "Response failed" , e) } }