mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-09-14 21:04:22 +02: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
|
Please submit the answers to the following questions in the README.md file
|
||||||
|
|
||||||
* How long did you spend on the task?
|
* _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_
|
~16h
|
||||||
* If you had more time (or if you were doing it in production) what would you have changed/improved?
|
* _Rate from 0-10, how would you assess the complexity of the task? _0 - very trivial_, _10 -
|
||||||
* Which parts of the code would you like to highlight?
|
extremely complex__
|
||||||
* In your opinion, which Google library was the best addition to the Android Dev world in the last
|
|
||||||
years?
|
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 🙂
|
> 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
|
### Time frame
|
||||||
|
|
||||||
|
@@ -1,22 +1,28 @@
|
|||||||
package dev.adriankuta.flights.ui.home.components
|
package dev.adriankuta.flights.ui.home.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
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.TripDate
|
||||||
import dev.adriankuta.flights.domain.types.TripFlight
|
import dev.adriankuta.flights.domain.types.TripFlight
|
||||||
import dev.adriankuta.flights.ui.home.SearchResultUiState
|
import dev.adriankuta.flights.ui.home.SearchResultUiState
|
||||||
|
import dev.adriankuta.flights.ui.home.extensions.hasFlights
|
||||||
import dev.adriankuta.flights.ui.home.sample.FlightPreviewParameterProvider
|
import dev.adriankuta.flights.ui.home.sample.FlightPreviewParameterProvider
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -48,9 +55,17 @@ internal fun SearchResults(
|
|||||||
is SearchResultUiState.Error -> item { Text("Error") }
|
is SearchResultUiState.Error -> item { Text("Error") }
|
||||||
SearchResultUiState.Idle -> Unit
|
SearchResultUiState.Idle -> Unit
|
||||||
SearchResultUiState.Loading -> item { Text("Loading") }
|
SearchResultUiState.Loading -> item { Text("Loading") }
|
||||||
is SearchResultUiState.Success -> items(uiState.flight.trips) { trip ->
|
is SearchResultUiState.Success -> {
|
||||||
TripCard(trip = trip, currency = uiState.flight.currency)
|
if (uiState.flight.hasFlights()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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,9 +93,11 @@ private fun TripCard(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
trip.dates.forEach { tripDate ->
|
trip.dates.forEachIndexed { index, tripDate ->
|
||||||
TripDateItem(tripDate = tripDate, currency = currency)
|
TripDateItem(tripDate = tripDate, currency = currency)
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
if (index != trip.dates.lastIndex) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchResultsPreview(@PreviewParameter(FlightPreviewParameterProvider::class) flight: Flight) {
|
private fun SearchResultsPreview(@PreviewParameter(FlightPreviewParameterProvider::class) flight: Flight) {
|
||||||
SearchResults(uiState = SearchResultUiState.Success(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,9 +71,21 @@ private fun StationsScreen(
|
|||||||
LaunchedEffect(selectedOriginAirport) {
|
LaunchedEffect(selectedOriginAirport) {
|
||||||
selectedOriginAirport?.let { airportInfo = it }
|
selectedOriginAirport?.let { airportInfo = it }
|
||||||
}
|
}
|
||||||
AirportInfoCompact(
|
Column {
|
||||||
data = airportInfo,
|
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(
|
OutlinedTextField(
|
||||||
@@ -82,7 +94,7 @@ private fun StationsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
placeholder = { Text(text = stringResource(R.string.search_airports)) },
|
placeholder = { Text(text = stringResource(hintRes)) },
|
||||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
|
@@ -45,6 +45,7 @@ internal class StationsScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun onAirportSelected(airport: AirportInfo) {
|
fun onAirportSelected(airport: AirportInfo) {
|
||||||
_selectedOriginAirport.value = airport
|
_selectedOriginAirport.value = airport
|
||||||
|
_searchQuery.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSelectedAirport() {
|
fun clearSelectedAirport() {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="stations_screen_title">Stations</string>
|
<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>
|
</resources>
|
||||||
|
Reference in New Issue
Block a user