-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add StarredScreen to display starred repositories #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,146 @@ | ||||||
| package dev.therealashik.github.profile | ||||||
|
|
||||||
| import androidx.compose.foundation.background | ||||||
| import androidx.compose.foundation.layout.* | ||||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||||
| import androidx.compose.foundation.lazy.items | ||||||
| import androidx.compose.foundation.shape.CircleShape | ||||||
| import androidx.compose.material.icons.Icons | ||||||
| import androidx.compose.material.icons.automirrored.filled.ArrowBack | ||||||
| import androidx.compose.material.icons.filled.Star | ||||||
| import androidx.compose.material3.* | ||||||
| import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.runtime.collectAsState | ||||||
| import androidx.compose.runtime.getValue | ||||||
| import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.draw.clip | ||||||
| import androidx.compose.ui.text.font.FontWeight | ||||||
| import dev.therealashik.github.Dimens | ||||||
| import github.composeapp.generated.resources.Res | ||||||
| import github.composeapp.generated.resources.content_description_back | ||||||
| import github.composeapp.generated.resources.retry | ||||||
| import github.composeapp.generated.resources.starred_title | ||||||
| import org.jetbrains.compose.resources.stringResource | ||||||
|
|
||||||
| @OptIn(ExperimentalMaterial3Api::class) | ||||||
| @Composable | ||||||
| fun StarredScreen( | ||||||
| viewModel: StarredViewModel, | ||||||
| onBack: () -> Unit | ||||||
| ) { | ||||||
| val uiState by viewModel.uiState.collectAsState() | ||||||
|
|
||||||
| Scaffold( | ||||||
| topBar = { | ||||||
| TopAppBar( | ||||||
| title = { Text(stringResource(Res.string.starred_title)) }, | ||||||
| navigationIcon = { | ||||||
| IconButton(onClick = onBack) { | ||||||
| Icon( | ||||||
| imageVector = Icons.AutoMirrored.Filled.ArrowBack, | ||||||
| contentDescription = stringResource(Res.string.content_description_back) | ||||||
| ) | ||||||
| } | ||||||
| }, | ||||||
| colors = TopAppBarDefaults.topAppBarColors( | ||||||
| containerColor = MaterialTheme.colorScheme.surface, | ||||||
| titleContentColor = MaterialTheme.colorScheme.onSurface, | ||||||
| navigationIconContentColor = MaterialTheme.colorScheme.onSurface | ||||||
| ) | ||||||
| ) | ||||||
| } | ||||||
| ) { innerPadding -> | ||||||
| when (val state = uiState) { | ||||||
| is StarredUiState.Loading -> { | ||||||
| Box( | ||||||
| modifier = Modifier.fillMaxSize().padding(innerPadding), | ||||||
| contentAlignment = Alignment.Center | ||||||
| ) { CircularProgressIndicator() } | ||||||
| } | ||||||
| is StarredUiState.Error -> { | ||||||
| Box( | ||||||
| modifier = Modifier.fillMaxSize().padding(innerPadding), | ||||||
| contentAlignment = Alignment.Center | ||||||
| ) { | ||||||
| Column(horizontalAlignment = Alignment.CenterHorizontally) { | ||||||
| Text(state.message, color = MaterialTheme.colorScheme.error) | ||||||
| Spacer(modifier = Modifier.height(Dimens.SpacingMedium)) | ||||||
| Button(onClick = viewModel::loadData) { Text(stringResource(Res.string.retry)) } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| is StarredUiState.Success -> { | ||||||
| LazyColumn( | ||||||
| modifier = Modifier | ||||||
| .fillMaxSize() | ||||||
| .padding(innerPadding), | ||||||
| contentPadding = PaddingValues(vertical = Dimens.SpacingMedium), | ||||||
| verticalArrangement = Arrangement.spacedBy(Dimens.SpacingMedium) | ||||||
| ) { | ||||||
| items(state.starredRepos) { repo -> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is recommended to provide a unique key for items in a
Suggested change
|
||||||
| ElevatedCard( | ||||||
| modifier = Modifier | ||||||
| .fillMaxWidth() | ||||||
| .padding(horizontal = Dimens.SpacingMedium), | ||||||
| colors = CardDefaults.elevatedCardColors( | ||||||
| containerColor = MaterialTheme.colorScheme.surfaceContainerLow | ||||||
| ) | ||||||
| ) { | ||||||
| Column( | ||||||
| modifier = Modifier | ||||||
| .fillMaxWidth() | ||||||
| .padding(Dimens.SpacingMedium) | ||||||
| ) { | ||||||
| Text( | ||||||
| text = repo.name, | ||||||
| style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), | ||||||
| color = MaterialTheme.colorScheme.onSurface | ||||||
| ) | ||||||
| if (repo.description != null) { | ||||||
| Spacer(modifier = Modifier.height(Dimens.SpacingSmall)) | ||||||
| Text( | ||||||
| text = repo.description, | ||||||
| style = MaterialTheme.typography.bodyMedium, | ||||||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||||||
| ) | ||||||
| } | ||||||
| Spacer(modifier = Modifier.height(Dimens.SpacingMedium)) | ||||||
| Row(verticalAlignment = Alignment.CenterVertically) { | ||||||
| Icon( | ||||||
| imageVector = Icons.Filled.Star, | ||||||
| contentDescription = null, | ||||||
| tint = MaterialTheme.colorScheme.onSurfaceVariant, | ||||||
| modifier = Modifier.size(Dimens.IconSizeSmall) | ||||||
| ) | ||||||
| Spacer(modifier = Modifier.width(Dimens.SpacingExtraSmall)) | ||||||
| Text( | ||||||
| text = repo.stars.toString(), | ||||||
| style = MaterialTheme.typography.bodySmall, | ||||||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||||||
| ) | ||||||
|
|
||||||
| if (repo.language != null) { | ||||||
| Spacer(modifier = Modifier.width(Dimens.SpacingMedium)) | ||||||
| Box( | ||||||
| modifier = Modifier | ||||||
| .size(Dimens.IndicatorSize) | ||||||
| .clip(CircleShape) | ||||||
| .background(MaterialTheme.colorScheme.primary) | ||||||
| ) | ||||||
| Spacer(modifier = Modifier.width(Dimens.SpacingExtraSmall)) | ||||||
| Text( | ||||||
| text = repo.language, | ||||||
| style = MaterialTheme.typography.bodySmall, | ||||||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package dev.therealashik.github.profile | ||
|
|
||
| sealed class StarredUiState { | ||
| data object Loading : StarredUiState() | ||
| data class Error(val message: String) : StarredUiState() | ||
| data class Success(val starredRepos: List<StarredRepoItem>) : StarredUiState() | ||
| } | ||
|
|
||
| data class StarredRepoItem( | ||
| val id: String, | ||
| val name: String, | ||
| val description: String?, | ||
| val stars: Int, | ||
| val language: String? | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package dev.therealashik.github.profile | ||
|
|
||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.viewModelScope | ||
| import dev.therealashik.github.data.GitHubApiClient | ||
| import dev.therealashik.github.data.createTokenStorage | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| class StarredViewModel : ViewModel() { | ||
| private val apiClient = GitHubApiClient(createTokenStorage()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| private val _uiState = MutableStateFlow<StarredUiState>(StarredUiState.Loading) | ||
| val uiState: StateFlow<StarredUiState> = _uiState.asStateFlow() | ||
|
|
||
| init { | ||
| loadData() | ||
| } | ||
|
|
||
| fun loadData() { | ||
| viewModelScope.launch { | ||
| _uiState.value = StarredUiState.Loading | ||
| apiClient.getStarredRepos(perPage = 100) | ||
| .onSuccess { repos -> | ||
| _uiState.value = StarredUiState.Success( | ||
| repos.map { r -> | ||
| StarredRepoItem( | ||
| id = r.id.toString(), | ||
| name = r.name, | ||
| description = r.description, | ||
| stars = r.stars, | ||
| language = r.language | ||
| ) | ||
| } | ||
| ) | ||
| } | ||
| .onFailure { _uiState.value = StarredUiState.Error(it.message ?: "Unknown error") } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
| override fun onCleared() { | ||
| super.onCleared() | ||
| apiClient.close() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instantiating the
ViewModeldirectly inside thecomposableblock causes a new instance to be created on every recomposition. Furthermore, since it's not obtained through aViewModelStoreOwner(e.g., usingviewModel()), itsonCleared()method will never be called, leading to a resource leak of theHttpClientinsideGitHubApiClient. Consider using a proper ViewModel provider that respects the navigation backstack lifecycle.