Skip to content

Commit

Permalink
add search screen body & search result UI & test OfflineRepository
Browse files Browse the repository at this point in the history
  • Loading branch information
nabilBouzineDev committed Aug 18, 2024
1 parent 5341424 commit 52a5d03
Show file tree
Hide file tree
Showing 22 changed files with 1,028 additions and 9 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
Expand Down
26 changes: 26 additions & 0 deletions app/src/androidTest/kotlin/data/local/dao/AirportDAOTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ private val airport2 = Airport(
7494765
)

// Sample sorted data by passengers from the actual database to test sorting
private val airport3 = Airport(
28,
"Munich International Airport",
"MUC",
47959885
)
private val airport4 = Airport(
7,
"Sheremetyevo - A.S. Pushkin international airport",
"SVO",
49933000
)

@RunWith(AndroidJUnit4::class)
class AirportDAOTest {

Expand Down Expand Up @@ -67,6 +81,18 @@ class AirportDAOTest {
assertEquals(allAirports[1], airport2)
}

@Test
@Throws(IOException::class)
fun daoGetAllAirportsOrderedByPassengers_returnAllAirportsWithCorrectSorting() = runBlocking {
val allAirports = airportDAO.getAllAirportsOrderedByPassengers().first()

assertTrue(allAirports.isNotEmpty())

assertTrue(airport3.passengers < airport4.passengers) // True So airport4 must be first
assertEquals(allAirports[0], airport4)
assertEquals(allAirports[1], airport3)
}

@Test
@Throws(IOException::class)
fun daoGetAirportByCode_returnSingleAirport() = runBlocking {
Expand Down
10 changes: 1 addition & 9 deletions app/src/main/java/com/nabilbdev/searchflight/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.nabilbdev.searchflight.ui.theme.SearchFlightTheme

Expand All @@ -23,12 +20,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surface
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Room database is ready!")
}
SearchFlightApp()
}
}
}
Expand Down
42 changes: 42 additions & 0 deletions app/src/main/java/com/nabilbdev/searchflight/SearchFlightApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.nabilbdev.searchflight

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.nabilbdev.searchflight.ui.AppViewModelProvider
import com.nabilbdev.searchflight.ui.screens.search.LoadUiState
import com.nabilbdev.searchflight.ui.screens.search.MySearchBar
import com.nabilbdev.searchflight.ui.screens.search.SearchScreen
import com.nabilbdev.searchflight.ui.screens.search.SearchScreenViewModel
import com.nabilbdev.searchflight.ui.screens.search.SearchUiState
import com.nabilbdev.searchflight.ui.screens.search.SelectUiState

@Composable
fun SearchFlightApp() {

val viewModel: SearchScreenViewModel = viewModel(factory = AppViewModelProvider.Factory)
val searchUiState: SearchUiState = viewModel.searchUiState.collectAsState().value
val selectUiState: SelectUiState = viewModel.selectUiState.collectAsState().value
val loadUiState: LoadUiState = viewModel.loadUiState.collectAsState().value

Scaffold(
topBar = {
MySearchBar(
query = searchUiState.searchQuery,
errorMessage = loadUiState.errorMessage,
allAirportsList = selectUiState.allAirportList,
airportListByQuery = searchUiState.airportListByQuery,
viewModel = viewModel
)
}
) { innerPadding ->
SearchScreen(
popularCitiesAirports = searchUiState.popularCityAirports,
isLoadingAirports = loadUiState.isLoadingPopularCityAirports,
modifier = Modifier.padding(innerPadding)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ interface AirportDAO {

@Query("SELECT * FROM airport")
fun getAllAirports(): Flow<List<Airport>>

@Query("SELECT * FROM airport ORDER BY passengers DESC")
fun getAllAirportsOrderedByPassengers(): Flow<List<Airport>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface SearchFlightRepository {

fun getAllAirportsStream(): Flow<List<Airport>>

fun getAllAirportsOrderedByPassengersStream(): Flow<List<Airport>>

suspend fun insertFavoriteAirport(favorite: Favorite)

suspend fun deleteFavoriteAirport(favorite: Favorite)
Expand All @@ -43,6 +45,9 @@ class OfflineSearchFlightRepository(
override fun getAllAirportsStream(): Flow<List<Airport>> =
airportDAO.getAllAirports()

override fun getAllAirportsOrderedByPassengersStream(): Flow<List<Airport>> =
airportDAO.getAllAirportsOrderedByPassengers()

override suspend fun insertFavoriteAirport(favorite: Favorite) =
favoriteDAO.insert(favorite)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.nabilbdev.searchflight.ui

import android.app.Application
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.nabilbdev.searchflight.SearchFlightApplication
import com.nabilbdev.searchflight.ui.screens.search.SearchScreenViewModel

/**
* Provides Factory to create instance of ViewModel for the entire SearchFlight app
*/
object AppViewModelProvider {
val Factory = viewModelFactory {
initializer {
SearchScreenViewModel(
searchFlightRepository = searchFLightApplication().container.searchFlightRepository
)
}
}
}

/**
* Extension function to queries for [Application] object and returns an instance of
* [SearchFlightApplication].
*/
fun CreationExtras.searchFLightApplication(): SearchFlightApplication =
(this[AndroidViewModelFactory.APPLICATION_KEY] as SearchFlightApplication)
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.nabilbdev.searchflight.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nabilbdev.searchflight.data.local.entity.Airport

@Composable
fun AirportCard(
airport: Airport,
imageVector: ImageVector,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.aspectRatio(4f / 2f),
shape = CardDefaults.elevatedShape
) {

Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
) {
Text(
text = airport.name,
fontWeight = FontWeight.Bold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.weight(0.5f)
)
Box(
modifier = Modifier
.weight(0.2f),
contentAlignment = Alignment.TopEnd
) {
Icon(
imageVector = imageVector,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(20.dp)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.nabilbdev.searchflight.ui.components

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
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.material3.LinearProgressIndicator
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.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.nabilbdev.searchflight.data.local.entity.Airport

@Composable
fun CommonAirportVerticalGrid(
titleContent: String,
airportList: List<Airport>,
imageVector: ImageVector,
modifier: Modifier = Modifier,
isLoadingAirports: Boolean = false
) {
AnimatedVisibility(visible = isLoadingAirports, exit = fadeOut()) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LinearProgressIndicator()
}
}
AnimatedVisibility(visible = !isLoadingAirports, enter = fadeIn()) {
CustomVerticalGrid(
titleContent = titleContent,
airportList = airportList,
imageVector = imageVector
)
}
}

/**
* Create a custom grid layout using a combination of Row and Column
*/
@Composable
fun CustomVerticalGrid(
titleContent: String,
airportList: List<Airport>,
imageVector: ImageVector,
modifier: Modifier = Modifier
) {
Column {
Text(text = titleContent, style = MaterialTheme.typography.titleLarge)
Spacer(modifier = modifier.height(8.dp))
airportList.chunked(2).forEach { halfList ->
Row(modifier = modifier.fillMaxWidth()) {
halfList.forEach { airport ->
AirportCard(
airport = airport,
imageVector = imageVector,
modifier = Modifier
.weight(1f)
.padding(4.dp)
)
}
}
}
}
}
Loading

0 comments on commit 52a5d03

Please sign in to comment.