Reputation: 317
When I am calling an API, it loads data into our LazyColumn, but only for the first time. When the user exits the app and comes back immediately, list is null. However, if they come back after a while, the data is shown again.
Additionally, it feels laggy in the LazyColumn, whereas when I call the same API in a RecyclerView, it doesn't feel laggy.
As I am new to Jetpack Compose, any advice or solutions would be appreciated.
When data is loaded for the first time, the component tree looks like this:
And the second time, the component tree looks like this:
MainActivity
class MainActivity : ComponentActivity() {
private val viewModel: EmployeeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface(
modifier = Modifier.fillMaxSize(),
) {
EmployeeList(viewModel)
}
}
}
}
@Composable
fun EmployeeList(viewModel: EmployeeViewModel) {
LaunchedEffect(Unit) {
viewModel.fetchEmployees()
}
val employee by viewModel.employees.observeAsState()
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp),
) {
when (val data = employee?.data) {
null -> LoadingIndicator(Modifier.align(Alignment.Center))
else -> {
if (data.any { it.employeeName.isBlank() || it.employeeAge == null || it.employeeSalary == null }) {
Text(
text = "Error: Invalid employee data!",
modifier = Modifier.align(Alignment.Center),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
} else {
LazyColumn {
items(data, key = { it.id }) { item ->
EmployeeCard(
profileImage = R.drawable.ic_launcher_background,
name = item.employeeName,
age = item.employeeAge.toString(),
salary = item.employeeSalary.toString(),
)
}
}
}
}
}
}
}
Retrofit Instance
object RetrofitInstance {
private const val BASE_URL = "https://dummy.restapiexample.com/api/v1/"
private val retrofit : Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val getEmployeeService : GetEmployeeService by lazy {
retrofit.create(GetEmployeeService::class.java)
}
}
Interface
interface GetEmployeeService {
@GET("employees")
suspend fun getEmployee() : Employee
}
ApiRepository
class ApiRepository {
private val employeeService = RetrofitInstance.getEmployeeService
suspend fun getEmployee() : Employee {
return employeeService.getEmployee()
}
}
ViewModel
class EmployeeViewModel : ViewModel(){
private val apiRepository = ApiRepository()
private val _employees = MutableLiveData<Employee>()
val employees : LiveData<Employee> = _employees
fun fetchEmployees() {
viewModelScope.launch {
try {
val empl = apiRepository.getEmployee()
_employees.value = empl
} catch (e: Exception) {
Log.d("Tag", "fetchEmployees: ${e.message}")
}
}
}
}
Upvotes: 2
Views: 239
Reputation: 15579
It appears that the API you call is rate-limited. That means that it will refuse any requests that are deemed too much with an HTTP 429
error code. What exactly "too much" means is internally defined by that endpoint. Usually it means a single IP address can only make a specified amount of requests in a given time frame.
This matches the behavior you describe: The first time your app is started (and therefore a request is made) everything works fine. So long as EmployeeList
is displayed no further requests are made.
When the user closes and reopens the app EmployeeList
is called again and another request is made. This is now refused by the API so instead of populating _employees
an error message is written to the log. In consequence observing viewModel.employees
will only return null
.
When the user waits for a sufficient amount of time so that the API will allow a new request everything seems to work fine again as it was the first time the app was opened.
If you do not have control over the API you will not be able to change the rate-limiting behavior. All you can do is to better handle that in your app. For starters, the user should see an appropritate error message like "Too many requests, please try again later." instead of a never ending LoadingIndicator
. You need to make changes to the catch
block to better differentiate null
from an error state.
That said, it will probably be more appropriate for your app to display old data instead of no data at all in the case the API denies a new request. To accomplish this you would need to persist the data you get from your first successful request. This way the data is stored locally in the filesystem. For as long as the app is installed the data will always be there. It is not affected by closing or restarting the app.
There arey various ways this can be done, the most common would be to use a database like Room or a DataStore. The latter comes in two flavors, Preferences DataStore and Proto DataStore. The first is designed to store simple objects like user preferences where the latter also handles more complex objects.
Choose the persistance frameworks that suits you best. In any case you will end up with a Flow in your repository that contains the stored data. Only display that flow's content in the UI. If you want to update the data (when the app is started or repeatedly in a given time interval or on demand when a button is pressed, or ...) make a request to the API and, when successful, persist the data according to the storage solution you chose. By the magic of the flow the UI will automatically be updated to the newest values. As a side effect the user can now close and reopen the app and still sees the same values, without a hanging LoadingIndicator
or an error message.
Regarding the LazyColumn's laggy behavior: This doesn't seem to be related to the above problem. You can search StackOverflow for answers, a cursory search brought up this, for example: Laggy Lazy Column Android Compose
If you cannot find an answer that solves your problem you can always ask a new question. In that case you can omit the repository, but you need to explain in more detail what EmployeeCard
is and how many entries you have. A minmal reproducible example would also be needed.
Upvotes: 0