如何用Retrofit解析API进度数据并构建Model?数据类型与检索方案
First, let's break down your JSON structure: you have a top-level progress object that maps course IDs to course data. Each course has fixed fields (name, type, progress) plus dynamic module IDs mapping to module data (which also has fixed fields).
To handle these dynamic keys while keeping type safety, we'll use maps for the dynamic parts and Gson's @JsonAnyGetter/@JsonAnySetter annotations to mix fixed and dynamic fields in our model classes.
Step 1: Define the Model Classes
1.1 Module Class (Fixed Fields)
This represents each module with its static properties:
data class Module( val name: String, val type: String, val progress: Int )
1.2 Course Class (Fixed + Dynamic Fields)
Each course has fixed fields plus dynamic module entries. We use @JsonAnySetter to capture unknown fields (the module IDs) into a map, and @JsonAnyGetter to expose them. We also add an operator method to enable bracket notation access:
import com.google.gson.annotations.JsonAnyGetter import com.google.gson.annotations.JsonAnySetter import com.google.gson.annotations.JsonIgnore data class Course( val name: String, val type: String, val progress: Double, @JsonIgnore private val modules: MutableMap<String, Module> = mutableMapOf() ) { // Capture any dynamic module fields into the modules map @JsonAnySetter fun setModule(moduleId: String, module: Module) { modules[moduleId] = module } // Expose the modules map for serialization (if needed) @JsonAnyGetter fun getModules(): Map<String, Module> = modules // Enable bracket notation access (e.g., course["moduleId"]) operator fun get(moduleId: String): Module? = modules[moduleId] }
1.3 Top-Level Response Class
This wraps the entire response, mapping course IDs to Course objects:
import com.google.gson.annotations.SerializedName data class ProgressResponse( @SerializedName("progress") val progress: Map<String, Course> )
Step 2: Set Up Retrofit
Make sure you have the Gson converter added to your Retrofit instance (this is standard for JSON parsing):
import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory val retrofit = Retrofit.Builder() .baseUrl("YOUR_API_BASE_URL") .addConverterFactory(GsonConverterFactory.create()) .build() // Define your API interface interface ProgressApi { @GET("your-endpoint") suspend fun getProgress(): retrofit2.Response<ProgressResponse> }
Step 3: Access the Data
Once you fetch the data, you can access it exactly like you requested using bracket notation:
// Fetch the response val response = progressApi.getProgress() if (response.isSuccessful) { val progressData = response.body()?.progress // Access course by ID val course = progressData?.get("5db0291a02306577129dc153") // Access module by ID using bracket notation val module = course?.get("5db028ab02306577129dc0c6") // Get the module's progress val moduleProgress = module?.progress // Will be 100 in your example }
Java Equivalent (If Needed)
If you're using Java, here's how the classes would look:
Module Class
public class Module { private String name; private String type; private int progress; // Getters and setters (or use Lombok for brevity) public String getName() { return name; } public void setName(String name) { this.name = name; } public String getType() { return type; } public void setType(String type) { this.type = type; } public int getProgress() { return progress; } public void setProgress(int progress) { this.progress = progress; } }
Course Class
import com.google.gson.annotations.JsonAnyGetter; import com.google.gson.annotations.JsonAnySetter; import com.google.gson.annotations.JsonIgnore; import java.util.HashMap; import java.util.Map; public class Course { private String name; private String type; private double progress; @JsonIgnore private Map<String, Module> modules = new HashMap<>(); // Getters and setters for fixed fields public String getName() { return name; } public void setName(String name) { this.name = name; } public String getType() { return type; } public void setType(String type) { this.type = type; } public double getProgress() { return progress; } public void setProgress(double progress) { this.progress = progress; } @JsonAnySetter public void setModule(String moduleId, Module module) { modules.put(moduleId, module); } @JsonAnyGetter public Map<String, Module> getModules() { return modules; } // Convenience method for bracket-like access (Java doesn't support operator overloading) public Module getModule(String moduleId) { return modules.get(moduleId); } }
Response Class
import com.google.gson.annotations.SerializedName; import java.util.Map; public class ProgressResponse { @SerializedName("progress") private Map<String, Course> progress; public Map<String, Course> getProgress() { return progress; } public void setProgress(Map<String, Course> progress) { this.progress = progress; } }
Accessing in Java:
ProgressResponse response = progressApi.getProgress().body(); if (response != null) { Map<String, Course> progressData = response.getProgress(); Course course = progressData.get("5db0291a02306577129dc153"); Module module = course.getModule("5db028ab02306577129dc0c6"); int moduleProgress = module.getProgress(); }
Key Notes
- The
@JsonAnySetterannotation captures all fields in the course object that aren't explicitly defined (the dynamic module IDs) and stores them in themodulesmap. - Using a
Map<String, Course>for the top-level progress field lets you directly access courses by their ID string. - The operator method in Kotlin enables the exact bracket notation you wanted (
course["moduleId"]), while Java uses a getter method as it doesn't support operator overloading.
内容的提问来源于stack exchange,提问作者Zubair Zahoor

