mirror of
				https://github.com/AdrianKuta/android-challange-adrian-kuta.git
				synced 2025-10-31 06:23:40 +01:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			8ce553240c
			...
			d7253b1218
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d7253b1218 | ||
|   | 8d850c72a8 | 
							
								
								
									
										42
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								README.md
									
									
									
									
									
								
							| @@ -64,17 +64,41 @@ ___ | ||||
|  | ||||
| Please submit the answers to the following questions in the README.md file | ||||
|  | ||||
| * How long did you spend on the task? | ||||
| * Rate from 0-10, how would you assess the complexity of the task? _0 - very trivial_, _10 - | ||||
|   extremely complex_ | ||||
| * If you had more time (or if you were doing it in production) what would you have changed/improved? | ||||
| * Which parts of the code would you like to highlight? | ||||
| * In your opinion, which Google library was the best addition to the Android Dev world in the last | ||||
|   years? | ||||
| * _How long did you spend on the task?_ | ||||
|  | ||||
|   ~16h | ||||
| * _Rate from 0-10, how would you assess the complexity of the task? _0 - very trivial_, _10 - | ||||
|   extremely complex__ | ||||
|  | ||||
|   Difficulty - 2 | ||||
|  | ||||
|   Complexity - 7 | ||||
| * _If you had more time (or if you were doing it in production) what would you have | ||||
|   changed/improved?_ | ||||
|     * I would remove hardcoded text values. | ||||
|     * I would add more tests. | ||||
|     * Improve `Cache`, to use also Room as cache e.g. for Airports, instead of memory cache. Right | ||||
|       there is no refresh mechanism for cache. | ||||
|     * Improve sort and group logic for Stations | ||||
|     * Of course UI, it can be more adaptive. Split UI implementation into smaller reusable | ||||
|       Composables. | ||||
| * _Which parts of the code would you like to highlight?_ | ||||
|     * App architecture - multimodularity, separation of concerns, etc. | ||||
| * _In your opinion, which Google library was the best addition to the Android Dev world in the last | ||||
|   years?_ | ||||
|     * In my opinion, Jetpack Compose has been the most impactful and transformative Google library | ||||
|       added to Android development in recent years. | ||||
|  | ||||
| > If you have the need to comment the provided task, we would love to hear that from you 🙂   | ||||
| > ...   | ||||
| > ... | ||||
| >  | ||||
| > The idea behind the task was quite straightforward, and I didn’t encounter any major difficulties | ||||
| > during the implementation. | ||||
| > However, given the limited time, I had to make some compromises, and the current solution is | ||||
| > certainly not perfect — a keen eye might spot some flaws. That said, I hope to demonstrate that the | ||||
| > application is well thought out in terms of its architecture, which was my main focus. | ||||
| > Each area — UI, Domain, and Model — has its own dedicated space with appropriate layers of | ||||
| > abstraction and dependency injection. In short, it’s a compact example of applying SOLID principles | ||||
| > in practice. | ||||
|  | ||||
| ### Time frame | ||||
|  | ||||
|   | ||||
| @@ -1,22 +1,28 @@ | ||||
| package dev.adriankuta.flights.ui.home.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.lazy.itemsIndexed | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.Search | ||||
| import androidx.compose.material3.Card | ||||
| import androidx.compose.material3.CardDefaults | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| import androidx.compose.ui.tooling.preview.PreviewParameter | ||||
| import androidx.compose.ui.unit.dp | ||||
| @@ -25,6 +31,7 @@ import dev.adriankuta.flights.domain.types.Trip | ||||
| import dev.adriankuta.flights.domain.types.TripDate | ||||
| import dev.adriankuta.flights.domain.types.TripFlight | ||||
| import dev.adriankuta.flights.ui.home.SearchResultUiState | ||||
| import dev.adriankuta.flights.ui.home.extensions.hasFlights | ||||
| import dev.adriankuta.flights.ui.home.sample.FlightPreviewParameterProvider | ||||
|  | ||||
| @Composable | ||||
| @@ -48,11 +55,19 @@ internal fun SearchResults( | ||||
|             is SearchResultUiState.Error -> item { Text("Error") } | ||||
|             SearchResultUiState.Idle -> Unit | ||||
|             SearchResultUiState.Loading -> item { Text("Loading") } | ||||
|             is SearchResultUiState.Success -> items(uiState.flight.trips) { trip -> | ||||
|             is SearchResultUiState.Success -> { | ||||
|                 if (uiState.flight.hasFlights()) { | ||||
|                     itemsIndexed(uiState.flight.trips) { index, trip -> | ||||
|                         TripCard(trip = trip, currency = uiState.flight.currency) | ||||
|                         if (index < uiState.flight.trips.lastIndex) { | ||||
|                             Spacer(modifier = Modifier.height(16.dp)) | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     item { EmptyListComponent() } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -78,14 +93,16 @@ private fun TripCard( | ||||
|             Spacer(modifier = Modifier.height(8.dp)) | ||||
|  | ||||
|             Column { | ||||
|                 trip.dates.forEach { tripDate -> | ||||
|                 trip.dates.forEachIndexed { index, tripDate -> | ||||
|                     TripDateItem(tripDate = tripDate, currency = currency) | ||||
|                     if (index != trip.dates.lastIndex) { | ||||
|                         HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun TripDateItem( | ||||
| @@ -163,8 +180,63 @@ private fun FlightItem( | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun EmptyListComponent( | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     Box( | ||||
|         modifier = modifier | ||||
|             .fillMaxSize() | ||||
|             .padding(16.dp), | ||||
|         contentAlignment = Alignment.Center, | ||||
|     ) { | ||||
|         Column( | ||||
|             horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         ) { | ||||
|             Icon( | ||||
|                 imageVector = Icons.Default.Search, | ||||
|                 contentDescription = null, | ||||
|                 modifier = Modifier.padding(bottom = 16.dp), | ||||
|                 tint = MaterialTheme.colorScheme.primary, | ||||
|             ) | ||||
|             Text( | ||||
|                 text = "No flights found", | ||||
|                 style = MaterialTheme.typography.titleMedium, | ||||
|                 fontWeight = FontWeight.Bold, | ||||
|                 textAlign = TextAlign.Center, | ||||
|             ) | ||||
|             Spacer(modifier = Modifier.height(8.dp)) | ||||
|             Text( | ||||
|                 text = "Try changing your search criteria", | ||||
|                 style = MaterialTheme.typography.bodyMedium, | ||||
|                 textAlign = TextAlign.Center, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Preview(showBackground = true) | ||||
| @Composable | ||||
| private fun EmptyListPreview() { | ||||
|     EmptyListComponent() | ||||
| } | ||||
|  | ||||
| @Preview(showBackground = true) | ||||
| @Composable | ||||
| private fun SearchResultsPreview(@PreviewParameter(FlightPreviewParameterProvider::class) flight: Flight) { | ||||
|     SearchResults(uiState = SearchResultUiState.Success(flight)) | ||||
| } | ||||
|  | ||||
| @Preview(showBackground = true) | ||||
| @Composable | ||||
| private fun SearchResultsEmptyPreview() { | ||||
|     SearchResults( | ||||
|         uiState = SearchResultUiState.Success( | ||||
|             Flight( | ||||
|                 currency = "EUR", | ||||
|                 currPrecision = 2, | ||||
|                 trips = emptyList(), | ||||
|             ), | ||||
|         ), | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| package dev.adriankuta.flights.ui.home.extensions | ||||
|  | ||||
| import dev.adriankuta.flights.domain.types.Flight | ||||
|  | ||||
| internal fun Flight.hasFlights(): Boolean = | ||||
|     trips.firstOrNull()?.dates?.firstOrNull()?.flights?.isNotEmpty() == true | ||||
| @@ -71,10 +71,22 @@ private fun StationsScreen( | ||||
|             LaunchedEffect(selectedOriginAirport) { | ||||
|                 selectedOriginAirport?.let { airportInfo = it } | ||||
|             } | ||||
|             Column { | ||||
|                 Text( | ||||
|                     text = "Origin airport", | ||||
|                     modifier = Modifier.padding(horizontal = 16.dp), | ||||
|                 ) | ||||
|                 AirportInfoCompact( | ||||
|                     data = airportInfo, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val hintRes = if (selectedOriginAirport == null) { | ||||
|             R.string.search_origin_airport | ||||
|         } else { | ||||
|             R.string.search_destination_airport | ||||
|         } | ||||
|  | ||||
|         OutlinedTextField( | ||||
|             value = searchQuery, | ||||
| @@ -82,7 +94,7 @@ private fun StationsScreen( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding(16.dp), | ||||
|             placeholder = { Text(text = stringResource(R.string.search_airports)) }, | ||||
|             placeholder = { Text(text = stringResource(hintRes)) }, | ||||
|             leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, | ||||
|             singleLine = true, | ||||
|         ) | ||||
|   | ||||
| @@ -45,6 +45,7 @@ internal class StationsScreenViewModel @Inject constructor( | ||||
|  | ||||
|     fun onAirportSelected(airport: AirportInfo) { | ||||
|         _selectedOriginAirport.value = airport | ||||
|         _searchQuery.value = "" | ||||
|     } | ||||
|  | ||||
|     fun clearSelectedAirport() { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="stations_screen_title">Stations</string> | ||||
|     <string name="search_airports">Search airports</string> | ||||
|     <string name="search_origin_airport">Search origin airport</string> | ||||
|     <string name="search_destination_airport">Search destination airport</string> | ||||
| </resources> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user