├── core
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── dimen.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── fonts.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── ic_utair_logo.png
│ │ │ │ ├── swap_cities_background_normal.png
│ │ │ │ ├── swap_cities_background_pressed.png
│ │ │ │ └── swap_cities_background_disabled.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── ic_utair_logo.png
│ │ │ │ ├── swap_cities_background_normal.png
│ │ │ │ ├── swap_cities_background_pressed.png
│ │ │ │ └── swap_cities_background_disabled.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── ic_utair_logo.png
│ │ │ │ ├── swap_cities_background_normal.png
│ │ │ │ ├── swap_cities_background_disabled.png
│ │ │ │ ├── swap_cities_background_pressed.png
│ │ │ │ └── toolbar_shadow.xml
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_utair_logo.png
│ │ │ │ ├── swap_cities_background_normal.png
│ │ │ │ ├── swap_cities_background_pressed.png
│ │ │ │ └── swap_cities_background_disabled.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── ic_utair_logo.png
│ │ │ │ ├── swap_cities_background_normal.png
│ │ │ │ └── swap_cities_background_pressed.png
│ │ │ ├── drawable-ldpi
│ │ │ │ ├── swap_cities_background_normal.png
│ │ │ │ └── swap_cities_background_pressed.png
│ │ │ ├── drawable
│ │ │ │ ├── blue_clickable_background_normal.xml
│ │ │ │ ├── find_flights_background_normal.xml
│ │ │ │ ├── white_clickable_background_normal.xml
│ │ │ │ ├── find_flights_background_pressed.xml
│ │ │ │ ├── horizontal_divider_white.xml
│ │ │ │ ├── set_return_date_button_background.xml
│ │ │ │ ├── ic_passenger_kid.xml
│ │ │ │ ├── ic_passenger_adult.xml
│ │ │ │ ├── ic_passenger_baby.xml
│ │ │ │ ├── ic_plane_back.xml
│ │ │ │ ├── ic_plane_forward.xml
│ │ │ │ ├── white_clickable_background.xml
│ │ │ │ ├── find_flights_background.xml
│ │ │ │ ├── blue_clickable_background.xml
│ │ │ │ ├── ic_arrow_back_blue_24dp.xml
│ │ │ │ ├── blue_clickable_background_pressed.xml
│ │ │ │ ├── white_clickable_background_pressed.xml
│ │ │ │ ├── ic_minus_10dp.xml
│ │ │ │ ├── ic_start_point.xml
│ │ │ │ ├── ic_plane_forward_blue.xml
│ │ │ │ ├── ic_plane_back_blue.xml
│ │ │ │ ├── ic_plane_back_gray.xml
│ │ │ │ ├── ic_plane_forward_blue_20x17.xml
│ │ │ │ ├── ic_plus_10dp.xml
│ │ │ │ ├── ic_plus_16dp.xml
│ │ │ │ ├── swap_cities_background.xml
│ │ │ │ ├── ic_passenger_adult_enabled.xml
│ │ │ │ ├── ic_passenger_kid_disabled.xml
│ │ │ │ ├── ic_passenger_kid_enabled.xml
│ │ │ │ ├── ic_plane_forward_gray.xml
│ │ │ │ ├── ic_info_outline.xml
│ │ │ │ ├── ic_passenger_adult_disabled.xml
│ │ │ │ ├── ic_end_point.xml
│ │ │ │ ├── ic_swap_cities.xml
│ │ │ │ ├── vertical_dashed_line_background.xml
│ │ │ │ ├── ic_clear_return_date.xml
│ │ │ │ ├── ic_passenger_baby_enabled.xml
│ │ │ │ ├── ic_passenger_baby_disabled.xml
│ │ │ │ ├── ic_sun.xml
│ │ │ │ ├── ic_fog.xml
│ │ │ │ ├── ic_question.xml
│ │ │ │ ├── ic_snowflake.xml
│ │ │ │ ├── ic_clouds.xml
│ │ │ │ └── ic_light_clouds.xml
│ │ │ ├── drawable-v21
│ │ │ │ ├── blue_clickable_background.xml
│ │ │ │ ├── white_clickable_background.xml
│ │ │ │ └── find_flights_background.xml
│ │ │ ├── color
│ │ │ │ ├── tab_text_color.xml
│ │ │ │ └── passenger_counter_value_color.xml
│ │ │ ├── menu
│ │ │ │ └── main_menu.xml
│ │ │ └── layout
│ │ │ │ ├── toolbar_blue.xml
│ │ │ │ ├── fragment_city_weather_forecast.xml
│ │ │ │ ├── activity_weather_forecast.xml
│ │ │ │ ├── item_daily_forecast.xml
│ │ │ │ ├── cities_forecasts_tabs.xml
│ │ │ │ ├── item_hourly_forecast.xml
│ │ │ │ ├── toolbar_white.xml
│ │ │ │ └── view_passenger_counter.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── utair
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── domain
│ │ │ │ ├── global
│ │ │ │ │ ├── models
│ │ │ │ │ │ ├── FlightOrderData.kt
│ │ │ │ │ │ └── WeatherForecast.kt
│ │ │ │ │ ├── exceptions
│ │ │ │ │ │ ├── PassengersDataValidationError.kt
│ │ │ │ │ │ ├── FlightOrderDataValidationError.kt
│ │ │ │ │ │ └── NoNetworkException.kt
│ │ │ │ │ └── ResourceManager.kt
│ │ │ │ └── flightorder
│ │ │ │ │ ├── FlightOrderDataValidator.kt
│ │ │ │ │ └── PassengersDataValidator.kt
│ │ │ │ ├── global
│ │ │ │ └── appinitializer
│ │ │ │ │ ├── AppInitializersProvider.kt
│ │ │ │ │ ├── AppInitializerElement.kt
│ │ │ │ │ ├── AppInitializer.kt
│ │ │ │ │ └── initialzers
│ │ │ │ │ └── AppConfigurationInitializer.kt
│ │ │ │ ├── presentation
│ │ │ │ ├── mvp
│ │ │ │ │ ├── flightorder
│ │ │ │ │ │ └── PassengersData.kt
│ │ │ │ │ └── global
│ │ │ │ │ │ ├── ErrorHandler.kt
│ │ │ │ │ │ └── AndroidResourceManager.kt
│ │ │ │ └── ui
│ │ │ │ │ ├── global
│ │ │ │ │ ├── ViewExtensions.kt
│ │ │ │ │ └── base
│ │ │ │ │ │ ├── MvpAppCompatActivity.kt
│ │ │ │ │ │ ├── MvpAppCompatDialogFragment.kt
│ │ │ │ │ │ └── MvpAppCompatFragment.kt
│ │ │ │ │ ├── weatherforecast
│ │ │ │ │ ├── CitiesForecastPagerAdapter.kt
│ │ │ │ │ ├── DailyForecastAdapter.kt
│ │ │ │ │ └── HourlyForecastAdapter.kt
│ │ │ │ │ └── flightorder
│ │ │ │ │ ├── PassengersCountPicker.kt
│ │ │ │ │ └── PassengerCounter.kt
│ │ │ │ └── data
│ │ │ │ └── global
│ │ │ │ ├── network
│ │ │ │ ├── responses
│ │ │ │ │ ├── WeatherForecastResponse.kt
│ │ │ │ │ └── CitiesListResponse.kt
│ │ │ │ ├── ApiConstants.kt
│ │ │ │ ├── NetworkChecker.kt
│ │ │ │ ├── WeatherApiInterceptor.kt
│ │ │ │ ├── NetworkCheckInterceptor.kt
│ │ │ │ ├── ApiBuilder.kt
│ │ │ │ ├── WeatherForecastDeserializer.kt
│ │ │ │ └── mappers
│ │ │ │ │ └── WeatherForecastResponseMapper.kt
│ │ │ │ └── UTairPreferences.kt
│ │ ├── assets
│ │ │ └── fonts
│ │ │ │ ├── OpenSans-Bold.ttf
│ │ │ │ ├── OpenSans-Light.ttf
│ │ │ │ ├── Roboto-Medium.ttf
│ │ │ │ ├── OpenSans-Regular.ttf
│ │ │ │ └── OpenSans-Semibold.ttf
│ │ └── AndroidManifest.xml
│ └── test
│ │ └── java
│ │ └── com
│ │ └── utair
│ │ └── domain
│ │ └── flightorder
│ │ ├── FlightOrderDataValidatorTest.kt
│ │ └── PassengersDataValidatorTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── app-coroutines
├── .gitignore
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── utair
│ │ │ │ ├── DI.kt
│ │ │ │ ├── di
│ │ │ │ ├── qualifiers
│ │ │ │ │ ├── ArriveCity.kt
│ │ │ │ │ ├── CityName.kt
│ │ │ │ │ └── DepartCity.kt
│ │ │ │ ├── PrimitiveWrapper.kt
│ │ │ │ ├── DomainModule.kt
│ │ │ │ ├── AppNavigationModule.kt
│ │ │ │ ├── AppModule.kt
│ │ │ │ ├── DiExtensions.kt
│ │ │ │ └── DataModule.kt
│ │ │ │ ├── presentation
│ │ │ │ ├── ui
│ │ │ │ │ ├── global
│ │ │ │ │ │ ├── navigation
│ │ │ │ │ │ │ ├── WeatherForecastScreen.kt
│ │ │ │ │ │ │ └── UTairNavigationFactory.kt
│ │ │ │ │ │ └── base
│ │ │ │ │ │ │ ├── mvp
│ │ │ │ │ │ │ └── BasePresenter.kt
│ │ │ │ │ │ │ └── BaseFragment.kt
│ │ │ │ │ └── weatherforecast
│ │ │ │ │ │ ├── CityWeatherForecastFragment.kt
│ │ │ │ │ │ └── WeatherForecastActivity.kt
│ │ │ │ └── mvp
│ │ │ │ │ ├── weatherforecast
│ │ │ │ │ ├── WeatherForecastView.kt
│ │ │ │ │ ├── CityWeatherForecastView.kt
│ │ │ │ │ ├── WeatherForecastPresenter.kt
│ │ │ │ │ └── CityWeatherForecastPresenter.kt
│ │ │ │ │ └── flightorder
│ │ │ │ │ └── FlightOrderView.kt
│ │ │ │ ├── data
│ │ │ │ ├── global
│ │ │ │ │ └── network
│ │ │ │ │ │ ├── UTairApiService.kt
│ │ │ │ │ │ └── WeatherApiService.kt
│ │ │ │ ├── cities
│ │ │ │ │ └── CityRepository.kt
│ │ │ │ └── weather
│ │ │ │ │ └── WeatherRepository.kt
│ │ │ │ ├── domain
│ │ │ │ ├── weatherforecast
│ │ │ │ │ └── WeatherForecastInteractor.kt
│ │ │ │ └── flightorder
│ │ │ │ │ └── MainInteractor.kt
│ │ │ │ └── UTairApplication.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── utair
│ │ │ ├── FlightOrderScreenTest.kt
│ │ │ └── screens
│ │ │ └── FlightOrderScreen.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── utair
│ │ └── presentation
│ │ └── mvp
│ │ ├── WeatherForecastPresenterTest.kt
│ │ └── CityWeatherForecastPresenterTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── app-rxjava
├── .gitignore
├── src
│ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── utair
│ │ │ ├── di
│ │ │ ├── scopes
│ │ │ │ └── Presenter.kt
│ │ │ ├── qualifiers
│ │ │ │ └── JobScheduler.kt
│ │ │ ├── presenters
│ │ │ │ ├── weatherforecast
│ │ │ │ │ ├── ArriveCity.kt
│ │ │ │ │ ├── DepartCity.kt
│ │ │ │ │ ├── cityforecast
│ │ │ │ │ │ ├── CityName.kt
│ │ │ │ │ │ ├── CityForecastPresenterModule.kt
│ │ │ │ │ │ └── CityForecastComponent.kt
│ │ │ │ │ ├── WeatherForecastPresenterModule.kt
│ │ │ │ │ └── WeatherForecastComponent.kt
│ │ │ │ └── orderflight
│ │ │ │ │ └── FlightOrderComponent.kt
│ │ │ ├── modules
│ │ │ │ ├── NavigationModule.kt
│ │ │ │ ├── ApplicationModule.kt
│ │ │ │ └── DataModule.kt
│ │ │ └── ApplicationComponent.kt
│ │ │ ├── presentation
│ │ │ ├── ui
│ │ │ │ ├── global
│ │ │ │ │ ├── navigation
│ │ │ │ │ │ ├── WeatherForecastScreen.kt
│ │ │ │ │ │ └── UTairNavigationFactory.kt
│ │ │ │ │ └── base
│ │ │ │ │ │ ├── BaseFragment.kt
│ │ │ │ │ │ ├── mvp
│ │ │ │ │ │ └── BasePresenter.kt
│ │ │ │ │ │ └── BaseMvpActivity.kt
│ │ │ │ └── weatherforecast
│ │ │ │ │ ├── CityWeatherForecastFragment.kt
│ │ │ │ │ └── WeatherForecastActivity.kt
│ │ │ └── mvp
│ │ │ │ ├── weatherforecast
│ │ │ │ ├── WeatherForecastView.kt
│ │ │ │ ├── CityWeatherForecastView.kt
│ │ │ │ ├── WeatherForecastPresenter.kt
│ │ │ │ └── CityWeatherForecastPresenter.kt
│ │ │ │ └── flightorder
│ │ │ │ └── FlightOrderView.kt
│ │ │ ├── data
│ │ │ ├── global
│ │ │ │ └── network
│ │ │ │ │ ├── UTairApiService.kt
│ │ │ │ │ └── WeatherApiService.kt
│ │ │ ├── cities
│ │ │ │ └── CityRepository.kt
│ │ │ └── weather
│ │ │ │ └── WeatherRepository.kt
│ │ │ ├── domain
│ │ │ ├── weatherforecast
│ │ │ │ └── WeatherForecastInteractor.kt
│ │ │ └── flightorder
│ │ │ │ └── MainInteractor.kt
│ │ │ └── UTairApplication.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── api_keys.properties
├── settings.gradle.kts
├── assets
└── splash.png
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew.bat
└── README.md
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app-coroutines/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-rxjava/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/api_keys.properties:
--------------------------------------------------------------------------------
1 | open_weather_api_key=YOUR_API_KEY
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(":core")
2 | include(":app-coroutines")
3 | include(":app-rxjava")
4 |
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/assets/splash.png
--------------------------------------------------------------------------------
/core/src/main/res/values/dimen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/DI.kt:
--------------------------------------------------------------------------------
1 | package com.utair
2 |
3 | object DI {
4 |
5 | const val APP_SCOPE = "app_scope"
6 |
7 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.utair
2 |
3 | object Constants {
4 | const val SUMMARY_MAX_PASSENGERS_COUNT = 9
5 | }
--------------------------------------------------------------------------------
/core/src/main/assets/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/assets/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/core/src/main/assets/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/assets/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/core/src/main/assets/fonts/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/assets/fonts/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/core/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/src/main/assets/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/assets/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/core/src/main/assets/fonts/OpenSans-Semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/assets/fonts/OpenSans-Semibold.ttf
--------------------------------------------------------------------------------
/core/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-hdpi/ic_utair_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-hdpi/ic_utair_logo.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-mdpi/ic_utair_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-mdpi/ic_utair_logo.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xhdpi/ic_utair_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xhdpi/ic_utair_logo.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxhdpi/ic_utair_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxhdpi/ic_utair_logo.png
--------------------------------------------------------------------------------
/core/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 55dp
4 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxxhdpi/ic_utair_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxxhdpi/ic_utair_logo.png
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/qualifiers/ArriveCity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class ArriveCity
7 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/qualifiers/CityName.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class CityName
7 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/qualifiers/DepartCity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class DepartCity
7 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable-hdpi/swap_cities_background_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-hdpi/swap_cities_background_normal.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-hdpi/swap_cities_background_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-hdpi/swap_cities_background_pressed.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-ldpi/swap_cities_background_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-ldpi/swap_cities_background_normal.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-ldpi/swap_cities_background_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-ldpi/swap_cities_background_pressed.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-mdpi/swap_cities_background_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-mdpi/swap_cities_background_normal.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-mdpi/swap_cities_background_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-mdpi/swap_cities_background_pressed.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xhdpi/swap_cities_background_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xhdpi/swap_cities_background_normal.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-hdpi/swap_cities_background_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-hdpi/swap_cities_background_disabled.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-mdpi/swap_cities_background_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-mdpi/swap_cities_background_disabled.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xhdpi/swap_cities_background_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xhdpi/swap_cities_background_disabled.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xhdpi/swap_cities_background_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xhdpi/swap_cities_background_pressed.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxhdpi/swap_cities_background_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxhdpi/swap_cities_background_normal.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxhdpi/swap_cities_background_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxhdpi/swap_cities_background_pressed.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxxhdpi/swap_cities_background_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxxhdpi/swap_cities_background_normal.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxhdpi/swap_cities_background_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxhdpi/swap_cities_background_disabled.png
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xxxhdpi/swap_cities_background_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/UTair-MVP-Sample/HEAD/core/src/main/res/drawable-xxxhdpi/swap_cities_background_pressed.png
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/global/models/FlightOrderData.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.global.models
2 |
3 | data class FlightOrderData(
4 | val departCity: String?,
5 | val arriveCity: String?
6 | )
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/global/appinitializer/AppInitializersProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utair.global.appinitializer
2 |
3 | interface AppInitializersProvider {
4 |
5 | fun getInitializers(): List
6 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/blue_clickable_background_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/find_flights_background_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/white_clickable_background_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/global/exceptions/PassengersDataValidationError.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.global.exceptions
2 |
3 | class PassengersDataValidationError(
4 | val errorMessage: String
5 | ) : Exception(errorMessage)
--------------------------------------------------------------------------------
/core/src/main/res/drawable/find_flights_background_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/global/exceptions/FlightOrderDataValidationError.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.global.exceptions
2 |
3 | class FlightOrderDataValidationError(
4 | val errorMessage: String
5 | ) : Exception(errorMessage)
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/mvp/flightorder/PassengersData.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.flightorder
2 |
3 | data class PassengersData(
4 | val adultCount: Int,
5 | val kidCount: Int,
6 | val babyCount: Int
7 | )
--------------------------------------------------------------------------------
/core/src/main/res/drawable/horizontal_divider_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/global/ResourceManager.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.global
2 |
3 | interface ResourceManager {
4 |
5 | fun getString(resourceId: Int, vararg args: Any): String
6 |
7 | fun getInteger(resourceId: Int): Int
8 |
9 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/global/exceptions/NoNetworkException.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.global.exceptions
2 |
3 | class NoNetworkException(
4 | message: String?,
5 | cause: Throwable? = null
6 | ) : RuntimeException(message, cause)
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/global/ViewExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global
2 |
3 | import android.view.View
4 |
5 | fun View.visible(visible: Boolean) {
6 | this.visibility = if (visible) View.VISIBLE else View.GONE
7 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/global/appinitializer/AppInitializerElement.kt:
--------------------------------------------------------------------------------
1 | package com.utair.global.appinitializer
2 |
3 | import android.content.Context
4 |
5 | interface AppInitializerElement {
6 |
7 | fun initialize(context: Context)
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/PrimitiveWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | /**
4 | * @author Konstantin Tskhovrebov (aka terrakok) on 09.07.17.
5 | */
6 | data class PrimitiveWrapper(val value: T) // see: https://youtrack.jetbrains.com/issue/KT-18918
7 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable-v21/blue_clickable_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable-v21/white_clickable_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/set_return_date_button_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/scopes/Presenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.scopes
2 |
3 | import java.lang.annotation.Retention
4 | import java.lang.annotation.RetentionPolicy
5 | import javax.inject.Scope
6 |
7 | @Scope
8 | @Retention(RetentionPolicy.RUNTIME)
9 | annotation class Presenter
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Sep 10 12:10:37 MSK 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
7 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable-xhdpi/toolbar_shadow.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/core/src/main/res/color/tab_text_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_kid.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_adult.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_baby.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_back.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/ui/global/navigation/WeatherForecastScreen.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.navigation
2 |
3 | import me.aartikov.alligator.Screen
4 | import java.io.Serializable
5 |
6 | class WeatherForecastScreen(
7 | var departCity: String,
8 | var arriveCity: String
9 | ) : Screen, Serializable
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/global/navigation/WeatherForecastScreen.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.navigation
2 |
3 | import me.aartikov.alligator.Screen
4 | import java.io.Serializable
5 |
6 | class WeatherForecastScreen(
7 | var departCity: String,
8 | var arriveCity: String
9 | ) : Screen, Serializable
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_forward.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/white_clickable_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/qualifiers/JobScheduler.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.qualifiers
2 |
3 | import java.lang.annotation.Documented
4 | import java.lang.annotation.Retention
5 | import java.lang.annotation.RetentionPolicy
6 | import javax.inject.Qualifier
7 |
8 | @Documented
9 | @Qualifier
10 | @Retention(RetentionPolicy.RUNTIME)
11 | annotation class JobScheduler
--------------------------------------------------------------------------------
/core/src/main/res/color/passenger_counter_value_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/find_flights_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/data/global/network/UTairApiService.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.utair.data.global.network.responses.CitiesListResponse
4 | import retrofit2.http.GET
5 |
6 | interface UTairApiService {
7 |
8 | @GET("cities?country=${ApiConstants.RUSSIA_ISO_CODE}")
9 | suspend fun getCities(): CitiesListResponse
10 |
11 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/blue_clickable_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/ArriveCity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast
2 |
3 | import java.lang.annotation.Documented
4 | import java.lang.annotation.Retention
5 | import java.lang.annotation.RetentionPolicy
6 | import javax.inject.Qualifier
7 |
8 | @Documented
9 | @Qualifier
10 | @Retention(RetentionPolicy.RUNTIME)
11 | annotation class ArriveCity
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/DepartCity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast
2 |
3 | import java.lang.annotation.Documented
4 | import java.lang.annotation.Retention
5 | import java.lang.annotation.RetentionPolicy
6 | import javax.inject.Qualifier
7 |
8 | @Documented
9 | @Qualifier
10 | @Retention(RetentionPolicy.RUNTIME)
11 | annotation class DepartCity
--------------------------------------------------------------------------------
/core/src/main/res/menu/main_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/data/global/network/UTairApiService.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.utair.data.global.network.responses.CitiesListResponse
4 | import io.reactivex.Single
5 | import retrofit2.http.GET
6 |
7 | interface UTairApiService {
8 |
9 | @GET("cities?country=${ApiConstants.RUSSIA_ISO_CODE}")
10 | fun getCities(): Single
11 |
12 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/cityforecast/CityName.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast.cityforecast
2 |
3 | import java.lang.annotation.Documented
4 | import java.lang.annotation.Retention
5 | import java.lang.annotation.RetentionPolicy
6 | import javax.inject.Qualifier
7 |
8 | @Documented
9 | @Qualifier
10 | @Retention(RetentionPolicy.RUNTIME)
11 | annotation class CityName
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/global/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base
2 |
3 | import com.utair.UTairApplication
4 |
5 | abstract class BaseFragment : MvpAppCompatFragment() {
6 |
7 | protected val appComponent by lazy { UTairApplication.component() }
8 | protected val screenResolver by lazy {
9 | appComponent.getScreenResolver()
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/cityforecast/CityForecastPresenterModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast.cityforecast
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 |
6 | @Module
7 | class CityForecastPresenterModule(
8 | private val cityName: String
9 | ) {
10 |
11 | @Provides
12 | @CityName
13 | fun provideCityName(): String = cityName
14 |
15 | }
--------------------------------------------------------------------------------
/core/src/main/res/values/fonts.xml:
--------------------------------------------------------------------------------
1 |
2 | fonts/OpenSans-Light.ttf
3 | fonts/OpenSans-Regular.ttf
4 | fonts/OpenSans-Semibold.ttf
5 | fonts/OpenSans-Bold.ttf
6 | fonts/Roboto-Medium.ttf
7 |
8 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable-v21/find_flights_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_arrow_back_blue_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/blue_clickable_background_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/white_clickable_background_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_minus_10dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/data/global/network/WeatherApiService.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.utair.data.global.network.responses.WeatherForecastResponse
4 | import retrofit2.http.GET
5 | import retrofit2.http.Query
6 |
7 | interface WeatherApiService {
8 |
9 | @GET("forecast?units=${ApiConstants.CELSIUS_METRICS_KEY}")
10 | suspend fun getWeather(@Query("q") cityName: String?): WeatherForecastResponse
11 |
12 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/responses/WeatherForecastResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network.responses
2 |
3 | class WeatherForecastResponse(
4 | val cityName: String,
5 | val hourlyForecasts: List
6 | ) {
7 |
8 | class HourlyForecast(
9 | val timestamp: Long,
10 | val weatherId: Int,
11 | val temperature: Float,
12 | val speed: Float
13 | )
14 |
15 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/responses/CitiesListResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network.responses
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | class CitiesListResponse {
6 |
7 | @SerializedName("results")
8 | lateinit var cities: List
9 | private set
10 |
11 | inner class City {
12 | @SerializedName("city")
13 | lateinit var name: String
14 | private set
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/data/global/network/WeatherApiService.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.utair.data.global.network.responses.WeatherForecastResponse
4 | import io.reactivex.Single
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | interface WeatherApiService {
9 |
10 | @GET("forecast?units=${ApiConstants.CELSIUS_METRICS_KEY}")
11 | fun getWeather(@Query("q") cityName: String?): Single
12 |
13 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/ApiConstants.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.utair.core.BuildConfig
4 |
5 | object ApiConstants {
6 | const val CITIES_BASE_URL = "http://api.meetup.com/2/"
7 | const val RUSSIA_ISO_CODE = "ru"
8 |
9 | const val OPEN_WEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/"
10 | const val API_KEY = BuildConfig.OPEN_WEATHER_API_KEY
11 | const val CELSIUS_METRICS_KEY = "metric"
12 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/data/cities/CityRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.cities
2 |
3 | import com.utair.data.global.network.UTairApiService
4 | import javax.inject.Inject
5 |
6 | class CityRepository @Inject constructor(
7 | private val utairApiService: UTairApiService
8 | ) {
9 |
10 | suspend fun getCitiesList(): List {
11 | val citiesRepose = utairApiService.getCities()
12 | return citiesRepose.cities.map { city -> city.name }
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_start_point.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/orderflight/FlightOrderComponent.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.orderflight
2 |
3 | import com.utair.di.ApplicationComponent
4 | import com.utair.di.scopes.Presenter
5 | import com.utair.presentation.mvp.flightorder.FlightOrderPresenter
6 | import dagger.Component
7 | import javax.inject.Singleton
8 |
9 | @Presenter
10 | @Component(dependencies = [ApplicationComponent::class])
11 | interface FlightOrderComponent {
12 |
13 | fun getFlightOrderPresenter(): FlightOrderPresenter
14 |
15 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/mvp/global/ErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.global
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.widget.Toast
6 | import timber.log.Timber
7 | import javax.inject.Inject
8 |
9 | class ErrorHandler @Inject constructor(
10 | private val context: Context
11 | ) {
12 |
13 | fun handle(throwable: Throwable) {
14 | Timber.e(throwable)
15 | Toast.makeText(context, throwable.message, Toast.LENGTH_SHORT).show()
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/WeatherForecastPresenterModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 |
6 | @Module
7 | class WeatherForecastPresenterModule(
8 | private val departCity: String,
9 | private val arriveCity: String
10 | ) {
11 |
12 | @Provides
13 | @DepartCity
14 | fun provideDepartCity(): String = departCity
15 |
16 | @Provides
17 | @ArriveCity
18 | fun provideArriveCity(): String = arriveCity
19 |
20 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/mvp/weatherforecast/WeatherForecastView.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.MvpView
4 | import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
5 | import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
6 |
7 | @StateStrategyType(AddToEndSingleStrategy::class)
8 | interface WeatherForecastView : MvpView {
9 |
10 | fun showForecastForCities(departCity: String, arriveCity: String)
11 |
12 | fun openForecastPage(position: Int)
13 |
14 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/mvp/weatherforecast/WeatherForecastView.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.MvpView
4 | import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
5 | import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
6 |
7 | @StateStrategyType(AddToEndSingleStrategy::class)
8 | interface WeatherForecastView : MvpView {
9 |
10 | fun showForecastForCities(departCity: String, arriveCity: String)
11 |
12 | fun openForecastPage(position: Int)
13 |
14 | }
--------------------------------------------------------------------------------
/core/src/main/res/layout/toolbar_blue.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/domain/weatherforecast/WeatherForecastInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.weatherforecast
2 |
3 | import com.utair.data.weather.WeatherRepository
4 | import com.utair.domain.global.models.WeatherForecast
5 | import javax.inject.Inject
6 |
7 | class WeatherForecastInteractor @Inject constructor(
8 | private val weatherRepository: WeatherRepository
9 | ) {
10 |
11 | suspend fun getWeatherForecastForCity(cityName: String): WeatherForecast {
12 | return weatherRepository.getWeatherForecastForCity(cityName)
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_forward_blue.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_back_blue.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_back_gray.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_forward_blue_20x17.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plus_10dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
16 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plus_16dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
16 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/swap_cities_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
9 |
12 |
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/NetworkChecker.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import javax.inject.Inject
6 |
7 | class NetworkChecker @Inject constructor(private val context: Context) {
8 |
9 | val isConnected: Boolean
10 | get() {
11 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
12 | val netInfo = cm.activeNetworkInfo
13 | return netInfo != null && netInfo.isConnectedOrConnecting
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_adult_enabled.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_kid_disabled.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_kid_enabled.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_plane_forward_gray.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/domain/weatherforecast/WeatherForecastInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.weatherforecast
2 |
3 | import com.utair.data.weather.WeatherRepository
4 | import com.utair.domain.global.models.WeatherForecast
5 | import io.reactivex.Single
6 | import javax.inject.Inject
7 |
8 | class WeatherForecastInteractor @Inject constructor(
9 | private val weatherRepository: WeatherRepository
10 | ) {
11 |
12 | fun getWeatherForecastForCity(cityName: String): Single {
13 | return weatherRepository.getWeatherForecastForCity(cityName)
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_info_outline.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
10 |
14 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_adult_disabled.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/WeatherForecastComponent.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast
2 |
3 | import com.utair.di.ApplicationComponent
4 | import com.utair.di.scopes.Presenter
5 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter
6 | import dagger.Component
7 |
8 | @Presenter
9 | @Component(
10 | dependencies = [ApplicationComponent::class],
11 | modules = [WeatherForecastPresenterModule::class]
12 | )
13 | interface WeatherForecastComponent {
14 |
15 | fun getWeatherForecastPresenter(): WeatherForecastPresenter
16 |
17 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/mvp/weatherforecast/CityWeatherForecastView.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.MvpView
4 | import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
5 | import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
6 | import com.utair.domain.global.models.WeatherForecast.DailyForecast
7 |
8 | @StateStrategyType(AddToEndSingleStrategy::class)
9 | interface CityWeatherForecastView : MvpView {
10 |
11 | fun showForecast(dailyForecasts: List)
12 |
13 | fun showNoNetworkMessage()
14 |
15 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/mvp/weatherforecast/CityWeatherForecastView.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.MvpView
4 | import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
5 | import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
6 | import com.utair.domain.global.models.WeatherForecast.DailyForecast
7 |
8 | @StateStrategyType(AddToEndSingleStrategy::class)
9 | interface CityWeatherForecastView : MvpView {
10 |
11 | fun showForecast(dailyForecasts: List)
12 |
13 | fun showNoNetworkMessage()
14 |
15 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_end_point.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/presenters/weatherforecast/cityforecast/CityForecastComponent.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.presenters.weatherforecast.cityforecast
2 |
3 | import com.utair.di.ApplicationComponent
4 | import com.utair.di.scopes.Presenter
5 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastPresenter
6 | import dagger.Component
7 |
8 | @Presenter
9 | @Component(
10 | dependencies = [ApplicationComponent::class],
11 | modules = [CityForecastPresenterModule::class]
12 | )
13 | interface CityForecastComponent {
14 |
15 | fun getCityWeatherForecastPresenter(): CityWeatherForecastPresenter
16 |
17 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/global/appinitializer/AppInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.utair.global.appinitializer
2 |
3 | import android.content.Context
4 | import javax.inject.Inject
5 |
6 | class AppInitializer @Inject constructor(
7 | private val context: Context,
8 | private val appInitializersProvider: AppInitializersProvider
9 | ) {
10 |
11 | private var isInitialized: Boolean = false
12 |
13 | fun initialize() {
14 | if (isInitialized) return
15 |
16 | appInitializersProvider.getInitializers().forEach {
17 | it.initialize(context)
18 | }
19 | isInitialized = true
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/mvp/global/AndroidResourceManager.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.global
2 |
3 | import android.content.Context
4 | import com.utair.domain.global.ResourceManager
5 | import javax.inject.Inject
6 |
7 | class AndroidResourceManager @Inject constructor(
8 | private val context: Context
9 | ) : ResourceManager {
10 |
11 | override fun getString(resourceId: Int, vararg args: Any): String {
12 | return context.resources.getString(resourceId, *args)
13 | }
14 |
15 | override fun getInteger(resourceId: Int): Int {
16 | return context.resources.getInteger(resourceId)
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_swap_cities.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
18 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/vertical_dashed_line_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
8 |
10 |
11 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/global/base/mvp/BasePresenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base.mvp
2 |
3 | import com.arellomobile.mvp.MvpPresenter
4 | import com.arellomobile.mvp.MvpView
5 | import io.reactivex.Completable
6 | import io.reactivex.disposables.CompositeDisposable
7 | import io.reactivex.disposables.Disposable
8 |
9 | open class BasePresenter : MvpPresenter() {
10 |
11 | private val compositeDisposable = CompositeDisposable()
12 |
13 | override fun onDestroy() {
14 | compositeDisposable.dispose()
15 | }
16 |
17 | protected fun Disposable.connect() {
18 | compositeDisposable.add(this)
19 | }
20 |
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/core/src/main/res/layout/fragment_city_weather_forecast.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_clear_return_date.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/ui/global/base/mvp/BasePresenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base.mvp
2 |
3 | import com.arellomobile.mvp.MvpPresenter
4 | import com.arellomobile.mvp.MvpView
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlin.coroutines.CoroutineContext
9 |
10 | open class BasePresenter : MvpPresenter(), CoroutineScope {
11 |
12 | private val parentJob = Job()
13 | override val coroutineContext: CoroutineContext
14 | get() = Dispatchers.Main + parentJob
15 |
16 | override fun onDestroy() {
17 | parentJob.cancel()
18 | }
19 |
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/data/cities/CityRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.cities
2 |
3 | import com.utair.data.global.network.UTairApiService
4 | import io.reactivex.Single
5 | import io.reactivex.android.schedulers.AndroidSchedulers
6 | import io.reactivex.schedulers.Schedulers
7 | import javax.inject.Inject
8 |
9 | class CityRepository @Inject constructor(
10 | private val utairApiService: UTairApiService
11 | ) {
12 |
13 | fun getCitiesList(): Single> {
14 | return utairApiService.getCities()
15 | .map { it.cities.map { city -> city.name } }
16 | .subscribeOn(Schedulers.io())
17 | .observeOn(AndroidSchedulers.mainThread())
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_baby_enabled.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
19 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_passenger_baby_disabled.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
19 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/WeatherApiInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 | import java.io.IOException
6 |
7 | class WeatherApiInterceptor : Interceptor {
8 |
9 | @Throws(IOException::class)
10 | override fun intercept(chain: Interceptor.Chain): Response {
11 | val original = chain.request()
12 | val originalHttpUrl = original.url
13 | val url = originalHttpUrl.newBuilder()
14 | .addQueryParameter("appid", ApiConstants.API_KEY)
15 | .build()
16 | val requestBuilder = original.newBuilder()
17 | .url(url)
18 |
19 | val request = requestBuilder.build()
20 | return chain.proceed(request)
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/data/weather/WeatherRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.weather
2 |
3 | import com.utair.data.global.network.WeatherApiService
4 | import com.utair.data.global.network.mappers.WeatherForecastResponseMapper
5 | import com.utair.domain.global.models.WeatherForecast
6 | import javax.inject.Inject
7 |
8 | class WeatherRepository @Inject constructor(
9 | private val weatherApiService: WeatherApiService,
10 | private val weatherForecastResponseMapper: WeatherForecastResponseMapper
11 | ) {
12 |
13 | suspend fun getWeatherForecastForCity(
14 | cityName: String
15 | ): WeatherForecast {
16 | val forecastResponse = weatherApiService.getWeather(cityName)
17 | return weatherForecastResponseMapper.map(forecastResponse)
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/NetworkCheckInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.utair.domain.global.exceptions.NoNetworkException
4 | import okhttp3.Interceptor
5 | import okhttp3.Response
6 | import java.io.IOException
7 | import javax.inject.Inject
8 |
9 | class NetworkCheckInterceptor @Inject constructor(
10 | private val networkChecker: NetworkChecker
11 | ) : Interceptor {
12 |
13 | @Throws(IOException::class)
14 | override fun intercept(chain: Interceptor.Chain): Response {
15 | val requestBuilder = chain.request().newBuilder()
16 | if (!networkChecker.isConnected) {
17 | throw NoNetworkException("No network connection")
18 | }
19 | return chain.proceed(requestBuilder.build())
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/DomainModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | import com.utair.domain.flightorder.FlightOrderDataValidator
4 | import com.utair.domain.flightorder.PassengersDataValidator
5 | import com.utair.domain.global.ResourceManager
6 | import com.utair.presentation.mvp.global.AndroidResourceManager
7 | import toothpick.config.Module
8 |
9 | class DomainModule : Module() {
10 |
11 | init {
12 | bind(PassengersDataValidator::class)
13 | .toProviderInstance { PassengersDataValidator(getGlobal()) }
14 | bind(FlightOrderDataValidator::class)
15 | .toProviderInstance { FlightOrderDataValidator(getGlobal()) }
16 |
17 | bind(ResourceManager::class)
18 | .toProviderInstance { AndroidResourceManager(getGlobal()) }
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app-coroutines/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app-coroutines/src/androidTest/java/com/utair/FlightOrderScreenTest.kt:
--------------------------------------------------------------------------------
1 | package com.utair
2 |
3 | import androidx.test.rule.ActivityTestRule
4 | import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
5 | import com.utair.presentation.ui.flightorder.MainActivity
6 | import com.utair.screens.FlightOrderScreen
7 | import org.junit.Rule
8 | import org.junit.Test
9 |
10 | class FlightOrderScreenTest : TestCase() {
11 |
12 | @get:Rule
13 | val activityTestRule = ActivityTestRule(MainActivity::class.java, true, false)
14 |
15 | @Test
16 | fun test() = run {
17 | step("Open Flight Order Screen") {
18 | activityTestRule.launchActivity(null)
19 | FlightOrderScreen {
20 | departCityButton {
21 | click()
22 | }
23 | }
24 | }
25 |
26 | }
27 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | android.enableJetifier=true
13 | android.useAndroidX=true
14 | org.gradle.jvmargs=-Xmx1536m
15 |
16 | # When configured, Gradle will run in incubating parallel mode.
17 | # This option should only be used with decoupled projects. More details, visit
18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
19 | # org.gradle.parallel=true
20 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/AppNavigationModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | import com.utair.presentation.ui.global.navigation.UTairNavigationFactory
4 | import me.aartikov.alligator.AndroidNavigator
5 | import me.aartikov.alligator.NavigationContextBinder
6 | import me.aartikov.alligator.Navigator
7 | import me.aartikov.alligator.ScreenResolver
8 | import toothpick.config.Module
9 |
10 | class AppNavigationModule : Module() {
11 |
12 | init {
13 | val androidNavigator = AndroidNavigator(UTairNavigationFactory())
14 |
15 | bind(Navigator::class.java).toInstance(androidNavigator)
16 | bind(AndroidNavigator::class.java).toInstance(androidNavigator)
17 | bind(NavigationContextBinder::class.java).toInstance(androidNavigator)
18 | bind(ScreenResolver::class.java).toInstance(androidNavigator.screenResolver)
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app-coroutines/src/androidTest/java/com/utair/screens/FlightOrderScreen.kt:
--------------------------------------------------------------------------------
1 | package com.utair.screens
2 |
3 | import com.agoda.kakao.text.KButton
4 | import com.kaspersky.kaspresso.screens.KScreen
5 | import com.utair.R
6 | import com.utair.presentation.ui.flightorder.MainActivity
7 |
8 | object FlightOrderScreen : KScreen() {
9 |
10 | override val layoutId: Int? = R.layout.activity_main
11 | override val viewClass: Class<*>? = MainActivity::class.java
12 |
13 | val departCityButton = KButton { withId(R.id.departCityButton) }
14 | val arriveCityButton = KButton { withId(R.id.arriveCityButton) }
15 | val swapCitiesButton = KButton { withId(R.id.swapCitiesButton) }
16 | val departDateButton = KButton { withId(R.id.departDateButton) }
17 | val returnDateButton = KButton { withId(R.id.returnDateButton) }
18 | val addReturnDateButton = KButton { withId(R.id.addReturnDateButton) }
19 |
20 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/global/appinitializer/initialzers/AppConfigurationInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.utair.global.appinitializer.initialzers
2 |
3 | import android.content.Context
4 | import androidx.appcompat.app.AppCompatDelegate
5 | import io.github.inflationx.calligraphy3.CalligraphyConfig
6 | import io.github.inflationx.calligraphy3.CalligraphyInterceptor
7 | import io.github.inflationx.viewpump.ViewPump
8 | import com.utair.global.appinitializer.AppInitializerElement
9 | import javax.inject.Inject
10 |
11 | class AppConfigurationInitializer @Inject constructor() : AppInitializerElement {
12 |
13 | override fun initialize(context: Context) {
14 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
15 |
16 | val calligraphyInterceptor = CalligraphyInterceptor(CalligraphyConfig.Builder().build())
17 | val viewPumpConfig = ViewPump.builder()
18 | .addInterceptor(calligraphyInterceptor)
19 | .build()
20 | ViewPump.init(viewPumpConfig)
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/data/weather/WeatherRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.weather
2 |
3 | import com.utair.data.global.network.WeatherApiService
4 | import com.utair.data.global.network.mappers.WeatherForecastResponseMapper
5 | import com.utair.domain.global.models.WeatherForecast
6 | import io.reactivex.Single
7 | import io.reactivex.android.schedulers.AndroidSchedulers
8 | import io.reactivex.schedulers.Schedulers
9 | import javax.inject.Inject
10 |
11 | class WeatherRepository @Inject constructor(
12 | private val weatherApiService: WeatherApiService,
13 | private val weatherForecastResponseMapper: WeatherForecastResponseMapper
14 | ) {
15 |
16 | fun getWeatherForecastForCity(
17 | cityName: String
18 | ): Single {
19 | return weatherApiService.getWeather(cityName)
20 | .map { weatherForecastResponseMapper.map(it) }
21 | .subscribeOn(Schedulers.io())
22 | .observeOn(AndroidSchedulers.mainThread())
23 |
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/modules/NavigationModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.modules
2 |
3 | import com.utair.presentation.ui.global.navigation.UTairNavigationFactory
4 | import dagger.Module
5 | import dagger.Provides
6 | import me.aartikov.alligator.AndroidNavigator
7 | import me.aartikov.alligator.NavigationContextBinder
8 | import me.aartikov.alligator.Navigator
9 | import me.aartikov.alligator.ScreenResolver
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | class NavigationModule {
14 |
15 | private val androidNavigator = AndroidNavigator(UTairNavigationFactory())
16 |
17 | @Provides
18 | @Singleton
19 | fun provideNavigator(): Navigator {
20 | return androidNavigator
21 | }
22 |
23 | @Provides
24 | @Singleton
25 | fun provideNavigationContextBinder(): NavigationContextBinder {
26 | return androidNavigator
27 | }
28 |
29 | @Provides
30 | @Singleton
31 | fun provideScreenResolver(): ScreenResolver {
32 | return androidNavigator.screenResolver
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/flightorder/FlightOrderDataValidator.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.flightorder
2 |
3 | import com.utair.core.R
4 | import com.utair.domain.global.ResourceManager
5 | import com.utair.domain.global.exceptions.FlightOrderDataValidationError
6 | import com.utair.domain.global.models.FlightOrderData
7 | import javax.inject.Inject
8 |
9 | class FlightOrderDataValidator @Inject constructor(
10 | private val resourceManager: ResourceManager
11 | ) {
12 |
13 | fun validate(flightOrderData: FlightOrderData) {
14 | if (flightOrderData.departCity == null) {
15 | throw validationError(R.string.depart_city_is_empty_message)
16 | }
17 |
18 | if (flightOrderData.arriveCity == null) {
19 | throw validationError(R.string.arrive_city_is_empty_message)
20 | }
21 | }
22 |
23 | private fun validationError(stringResId: Int): FlightOrderDataValidationError {
24 | return FlightOrderDataValidationError(resourceManager.getString(stringResId))
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app-rxjava/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\AndroidDevelopment\Programms\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.kts.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/core/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/color_blue_dark
4 | @color/color_blue_dark
5 | @color/color_blue
6 |
7 | #ffffff
8 | #AAffffff
9 | #1156d7
10 | #114ab5
11 | #0e3f9c
12 | #ffffff
13 | #AAffffff
14 | #eeeeee
15 |
16 | #8090a6
17 | #0d49b5
18 | #eeeeee
19 | @color/text_white
20 | @color/text_white50
21 |
22 |
23 |
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_sun.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
14 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/mvp/weatherforecast/WeatherForecastPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.InjectViewState
4 | import com.arellomobile.mvp.MvpPresenter
5 | import com.utair.di.presenters.weatherforecast.ArriveCity
6 | import com.utair.di.presenters.weatherforecast.DepartCity
7 | import javax.inject.Inject
8 |
9 | @InjectViewState
10 | class WeatherForecastPresenter @Inject constructor(
11 | @DepartCity private val departCity: String,
12 | @ArriveCity private val arriveCity: String
13 | ) : MvpPresenter() {
14 |
15 | override fun onFirstViewAttach() {
16 | super.onFirstViewAttach()
17 |
18 | viewState.showForecastForCities(departCity, arriveCity)
19 | onTabSelected(DEPART_CITY_TAB_POSITION)
20 | }
21 |
22 | fun onTabSelected(position: Int) {
23 | viewState.openForecastPage(position)
24 | }
25 |
26 | companion object {
27 | const val DEPART_CITY_TAB_POSITION = 0
28 | const val ARRIVE_CITY_TAB_POSITION = 1
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/global/models/WeatherForecast.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.global.models
2 |
3 | import org.joda.time.DateTime
4 | import java.util.*
5 |
6 | class WeatherForecast(
7 | val cityName: String,
8 | val dailyForecasts: List
9 | ) {
10 |
11 | class DailyForecast(val date: DateTime) {
12 |
13 | private val hourlyForecasts: MutableList
14 |
15 | init {
16 | hourlyForecasts = ArrayList()
17 | }
18 |
19 | fun addHourForecast(hourlyForecast: HourlyForecast) {
20 | hourlyForecasts.add(hourlyForecast)
21 | }
22 |
23 | fun getHourlyForecasts(): List {
24 | return hourlyForecasts
25 | }
26 |
27 | }
28 |
29 | class HourlyForecast(
30 | val dateTime: DateTime,
31 | val condition: WeatherCondition,
32 | val temperature: Float,
33 | val speed: Float
34 | )
35 |
36 | enum class WeatherCondition {
37 | SUNNY, FOG, LIGHT_CLOUDS, CLOUDS, LIGHT_RAIN, RAIN, SNOW, STORM, UNKNOWN
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/core/src/main/res/layout/activity_weather_forecast.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
15 |
16 |
19 |
20 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/mvp/weatherforecast/WeatherForecastPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.InjectViewState
4 | import com.arellomobile.mvp.MvpPresenter
5 | import com.utair.di.PrimitiveWrapper
6 | import com.utair.di.qualifiers.ArriveCity
7 | import com.utair.di.qualifiers.DepartCity
8 | import javax.inject.Inject
9 |
10 | @InjectViewState
11 | class WeatherForecastPresenter @Inject constructor(
12 | @DepartCity private val departCity: PrimitiveWrapper,
13 | @ArriveCity private val arriveCity: PrimitiveWrapper
14 | ) : MvpPresenter() {
15 |
16 | override fun onFirstViewAttach() {
17 | super.onFirstViewAttach()
18 |
19 | viewState.showForecastForCities(departCity.value, arriveCity.value)
20 | onTabSelected(DEPART_CITY_TAB_POSITION)
21 | }
22 |
23 | fun onTabSelected(position: Int) {
24 | viewState.openForecastPage(position)
25 | }
26 |
27 | companion object {
28 | const val DEPART_CITY_TAB_POSITION = 0
29 | const val ARRIVE_CITY_TAB_POSITION = 1
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | import android.content.Context
4 | import com.utair.global.appinitializer.AppInitializer
5 | import com.utair.global.appinitializer.AppInitializerElement
6 | import com.utair.global.appinitializer.AppInitializersProvider
7 | import com.utair.global.appinitializer.initialzers.AppConfigurationInitializer
8 | import com.utair.presentation.mvp.global.ErrorHandler
9 | import toothpick.config.Module
10 |
11 | class AppModule(context: Context) : Module() {
12 |
13 | init {
14 | bind(Context::class).toInstance(context)
15 |
16 | bind(ErrorHandler::class)
17 | .toProviderInstance { ErrorHandler(getGlobal()) }
18 |
19 | val appInitializersProvider = object : AppInitializersProvider {
20 | override fun getInitializers(): List {
21 | val appConfigurationInitializer = AppConfigurationInitializer()
22 | return listOf(appConfigurationInitializer)
23 | }
24 | }
25 | bind(AppInitializer::class)
26 | .toInstance(AppInitializer(context, appInitializersProvider))
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/weatherforecast/CitiesForecastPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentManager
5 | import androidx.fragment.app.FragmentPagerAdapter
6 |
7 | class CitiesForecastPagerAdapter(
8 | fragmentManager: FragmentManager,
9 | private val departCityName: String,
10 | private val arriveCityName: String,
11 | private val fragmentConstructor: (name: String) -> Fragment
12 | ) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
13 |
14 | override fun getItem(position: Int): Fragment {
15 | return when (position) {
16 | DEPART_CITY_POSITION -> fragmentConstructor(departCityName)
17 | ARRIVE_CITY_POSITION -> fragmentConstructor(arriveCityName)
18 | else -> throw IllegalStateException()
19 | }
20 | }
21 |
22 | override fun getCount(): Int {
23 | return TABS_COUNT
24 | }
25 |
26 | companion object {
27 | const val TABS_COUNT = 2
28 | const val DEPART_CITY_POSITION = 0
29 | const val ARRIVE_CITY_POSITION = 1
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/core/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UTair
3 | Help
4 | Depart on
5 | Return on
6 | Departure
7 | Arrive
8 | Find flights
9 | From
10 | To
11 | All airports
12 |
13 | Adult
14 | 2–12 years
15 | 0–2 years
16 |
17 | Select_city
18 | Passengers must be no more than %d people
19 | There shouldn\'t be more babies than adults
20 | Select depart city
21 | Select arrive city
22 |
23 | Check your internet connection
24 |
25 |
26 |
--------------------------------------------------------------------------------
/core/src/main/res/layout/item_daily_forecast.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
21 |
22 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/domain/flightorder/MainInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.flightorder
2 |
3 | import com.utair.data.cities.CityRepository
4 | import com.utair.data.global.UTairPreferences
5 | import com.utair.domain.global.models.FlightOrderData
6 | import com.utair.presentation.mvp.flightorder.PassengersData
7 | import javax.inject.Inject
8 |
9 | class MainInteractor @Inject constructor(
10 | private val prefs: UTairPreferences,
11 | private val passengersDataValidator: PassengersDataValidator,
12 | private val flightOrderDataValidator: FlightOrderDataValidator,
13 | private val cityRepository: CityRepository
14 | ) {
15 |
16 | fun getFlightOrderData(): FlightOrderData {
17 | return prefs.getFlightOrderData()
18 | }
19 |
20 | fun saveFlightOrderData(flightOrderData: FlightOrderData) {
21 | prefs.saveOrderData(flightOrderData)
22 | }
23 |
24 | suspend fun getCities(): List {
25 | return cityRepository.getCitiesList()
26 | }
27 |
28 | fun validatePassengersData(data: PassengersData) {
29 | return passengersDataValidator.validate(data)
30 | }
31 |
32 | fun validateFlightOrderData(data: FlightOrderData) {
33 | return flightOrderDataValidator.validate(data)
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/domain/flightorder/MainInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.flightorder
2 |
3 | import com.utair.data.cities.CityRepository
4 | import com.utair.data.global.UTairPreferences
5 | import com.utair.domain.global.models.FlightOrderData
6 | import com.utair.presentation.mvp.flightorder.PassengersData
7 | import io.reactivex.Single
8 | import javax.inject.Inject
9 |
10 | class MainInteractor @Inject constructor(
11 | private val prefs: UTairPreferences,
12 | private val passengersDataValidator: PassengersDataValidator,
13 | private val flightOrderDataValidator: FlightOrderDataValidator,
14 | private val cityRepository: CityRepository
15 | ) {
16 |
17 | fun getFlightOrderData(): FlightOrderData {
18 | return prefs.getFlightOrderData()
19 | }
20 |
21 | fun saveFlightOrderData(flightOrderData: FlightOrderData) {
22 | prefs.saveOrderData(flightOrderData)
23 | }
24 |
25 | fun getCities(): Single> {
26 | return cityRepository.getCitiesList()
27 | }
28 |
29 | fun validatePassengersData(data: PassengersData) {
30 | return passengersDataValidator.validate(data)
31 | }
32 |
33 | fun validateData(data: FlightOrderData) {
34 | return flightOrderDataValidator.validate(data)
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/ApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | import android.content.Context
4 | import com.utair.data.cities.CityRepository
5 | import com.utair.data.weather.WeatherRepository
6 | import com.utair.di.modules.ApplicationModule
7 | import com.utair.di.modules.DataModule
8 | import com.utair.di.modules.NavigationModule
9 | import com.utair.domain.global.ResourceManager
10 | import com.utair.presentation.ui.global.navigation.UTairNavigationFactory
11 | import dagger.Component
12 | import me.aartikov.alligator.NavigationContextBinder
13 | import me.aartikov.alligator.Navigator
14 | import me.aartikov.alligator.ScreenResolver
15 | import javax.inject.Singleton
16 |
17 | @Singleton
18 | @Component(modules = [
19 | ApplicationModule::class,
20 | NavigationModule::class,
21 | DataModule::class
22 | ])
23 | interface ApplicationComponent {
24 |
25 | fun getContext(): Context
26 |
27 | fun getResourceManager(): ResourceManager
28 |
29 | fun getNavigator(): Navigator
30 |
31 | fun getNavigationFactory(): UTairNavigationFactory
32 |
33 | fun getScreenResolver(): ScreenResolver
34 |
35 | fun getNavigationContextBinder() : NavigationContextBinder
36 |
37 | fun getWeatherRepository(): WeatherRepository
38 |
39 | fun getCityRepository(): CityRepository
40 |
41 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/global/navigation/UTairNavigationFactory.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.navigation
2 |
3 | import android.app.Activity
4 | import androidx.fragment.app.DialogFragment
5 | import com.utair.presentation.ui.weatherforecast.WeatherForecastActivity
6 | import me.aartikov.alligator.Screen
7 | import me.aartikov.alligator.ScreenResult
8 | import me.aartikov.alligator.navigationfactories.RegistryNavigationFactory
9 | import javax.inject.Inject
10 |
11 | class UTairNavigationFactory @Inject constructor() : RegistryNavigationFactory() {
12 |
13 | init {
14 | bindScreen()
15 | }
16 |
17 | inline fun bindScreen() =
18 | registerActivity(S::class.java, A::class.java)
19 |
20 | inline fun bindScreenWithResult() =
21 | registerActivityForResult(S::class.java, A::class.java, R::class.java)
22 |
23 | inline fun bindDialog() = registerDialogFragment(S::class.java, D::class.java)
24 |
25 | inline fun bindResultDialog() =
26 | registerDialogFragmentForResult(S::class.java, D::class.java, R::class.java)
27 |
28 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/ui/global/navigation/UTairNavigationFactory.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.navigation
2 |
3 | import android.app.Activity
4 | import androidx.fragment.app.DialogFragment
5 | import com.utair.presentation.ui.weatherforecast.WeatherForecastActivity
6 | import me.aartikov.alligator.Screen
7 | import me.aartikov.alligator.ScreenResult
8 | import me.aartikov.alligator.navigationfactories.RegistryNavigationFactory
9 | import javax.inject.Inject
10 |
11 | class UTairNavigationFactory @Inject constructor() : RegistryNavigationFactory() {
12 |
13 | init {
14 | bindScreen()
15 | }
16 |
17 | inline fun bindScreen() =
18 | registerActivity(S::class.java, A::class.java)
19 |
20 | inline fun bindScreenWithResult() =
21 | registerActivityForResult(S::class.java, A::class.java, R::class.java)
22 |
23 | inline fun bindDialog() = registerDialogFragment(S::class.java, D::class.java)
24 |
25 | inline fun bindResultDialog() =
26 | registerDialogFragmentForResult(S::class.java, D::class.java, R::class.java)
27 |
28 | }
--------------------------------------------------------------------------------
/core/src/main/res/layout/cities_forecasts_tabs.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
19 |
20 |
21 |
22 |
28 |
29 |
32 |
33 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/core/src/main/res/layout/item_hourly_forecast.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
18 |
19 |
25 |
26 |
32 |
33 |
34 |
40 |
41 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/UTairPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import com.utair.domain.global.models.FlightOrderData
6 | import javax.inject.Inject
7 |
8 | class UTairPreferences @Inject constructor(context: Context) {
9 |
10 | private val appPreferences: SharedPreferences
11 |
12 | fun getFlightOrderData(): FlightOrderData {
13 | val departCity = appPreferences.getString(DEPART_CITY_KEY, "")!!
14 | val arriveCity = appPreferences.getString(ARRIVE_CITY_KEY, "")!!
15 | return FlightOrderData(departCity, arriveCity)
16 | }
17 |
18 | fun saveOrderData(flightOrderData: FlightOrderData) {
19 | if (flightOrderData.departCity != null) {
20 | appPreferences.edit().putString(DEPART_CITY_KEY, flightOrderData.departCity).apply()
21 | }
22 | if (flightOrderData.arriveCity != null) {
23 | appPreferences.edit().putString(ARRIVE_CITY_KEY, flightOrderData.arriveCity).apply()
24 | }
25 | }
26 |
27 | companion object {
28 | private const val APP_PREFS_FILE_NAME = "app_preferences"
29 | private const val DEPART_CITY_KEY = "depart_city_key"
30 | private const val ARRIVE_CITY_KEY = "arrive_city_key"
31 | }
32 |
33 | init {
34 | appPreferences = context.getSharedPreferences(APP_PREFS_FILE_NAME, Context.MODE_PRIVATE)
35 | }
36 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/domain/flightorder/PassengersDataValidator.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.flightorder
2 |
3 | import com.utair.Constants
4 | import com.utair.core.R
5 | import com.utair.domain.global.ResourceManager
6 | import com.utair.domain.global.exceptions.PassengersDataValidationError
7 | import com.utair.presentation.mvp.flightorder.PassengersData
8 | import javax.inject.Inject
9 |
10 | class PassengersDataValidator @Inject constructor(
11 | private val resourceManager: ResourceManager
12 | ) {
13 |
14 | fun validate(passengersData: PassengersData) {
15 | val summaryCount = passengersData.run {
16 | adultCount + kidCount + babyCount
17 | }
18 | val isMaxValueReached = summaryCount > Constants.SUMMARY_MAX_PASSENGERS_COUNT
19 | if (isMaxValueReached) {
20 | throw validationError(
21 | R.string.max_passenger_count_message,
22 | Constants.SUMMARY_MAX_PASSENGERS_COUNT
23 | )
24 | }
25 |
26 | if (passengersData.babyCount > passengersData.adultCount) {
27 | throw validationError(R.string.babies_less_adults_message)
28 | }
29 | }
30 |
31 | private fun validationError(
32 | stringResId: Int,
33 | vararg args: Any
34 | ): PassengersDataValidationError {
35 | val message = resourceManager.getString(stringResId, *args)
36 | return PassengersDataValidationError(message)
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/global/base/MvpAppCompatActivity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.arellomobile.mvp.MvpDelegate
6 |
7 | /**
8 | * Date: 17.12.2015
9 | * Time: 14:34
10 | *
11 | * @author Yuri Shmakov
12 | * @author Alexander Bliniov
13 | * @author Konstantin Tckhovrebov
14 | */
15 | open class MvpAppCompatActivity : AppCompatActivity() {
16 |
17 | private val mvpDelegate by lazy {
18 | MvpDelegate(this)
19 | }
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | mvpDelegate.onCreate(savedInstanceState)
24 | }
25 |
26 | override fun onStart() {
27 | super.onStart()
28 | mvpDelegate.onAttach()
29 | }
30 |
31 | override fun onResume() {
32 | super.onResume()
33 | mvpDelegate.onAttach()
34 | }
35 |
36 | override fun onSaveInstanceState(outState: Bundle) {
37 | super.onSaveInstanceState(outState)
38 | mvpDelegate.onSaveInstanceState(outState)
39 | mvpDelegate.onDetach()
40 | }
41 |
42 | override fun onStop() {
43 | super.onStop()
44 | mvpDelegate.onDetach()
45 | }
46 |
47 | override fun onDestroy() {
48 | super.onDestroy()
49 | mvpDelegate.onDestroyView()
50 | if (isFinishing) {
51 | mvpDelegate.onDestroy()
52 | }
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/mvp/weatherforecast/CityWeatherForecastPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.InjectViewState
4 | import com.utair.di.PrimitiveWrapper
5 | import com.utair.di.qualifiers.CityName
6 | import com.utair.domain.global.exceptions.NoNetworkException
7 | import com.utair.domain.weatherforecast.WeatherForecastInteractor
8 | import com.utair.presentation.mvp.global.ErrorHandler
9 | import com.utair.presentation.ui.global.base.mvp.BasePresenter
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 | @InjectViewState
14 | class CityWeatherForecastPresenter @Inject constructor(
15 | @CityName private val cityName: PrimitiveWrapper,
16 | private val weatherForecastInteractor: WeatherForecastInteractor,
17 | private val errorHandler: ErrorHandler
18 | ) : BasePresenter() {
19 |
20 | override fun onFirstViewAttach() {
21 | super.onFirstViewAttach()
22 |
23 | showForecast()
24 | }
25 |
26 | private fun showForecast() = launch {
27 | try {
28 | val forecast = weatherForecastInteractor.getWeatherForecastForCity(cityName.value)
29 | viewState.showForecast(forecast.dailyForecasts)
30 | } catch (error: Exception) {
31 | if (error is NoNetworkException) {
32 | viewState.showNoNetworkMessage()
33 | } else {
34 | errorHandler.handle(error)
35 | }
36 | }
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/ApiBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.GsonBuilder
5 | import okhttp3.OkHttpClient
6 | import retrofit2.CallAdapter
7 | import retrofit2.Retrofit
8 | import retrofit2.converter.gson.GsonConverterFactory
9 | import kotlin.reflect.KClass
10 |
11 | class ApiBuilder {
12 |
13 | private var okHttpClient: OkHttpClient? = null
14 | private var gson: Gson = GsonBuilder().create()
15 | private var callAdapter: CallAdapter.Factory? = null
16 |
17 | fun okHttpClient(okHttpClient: OkHttpClient): ApiBuilder {
18 | this.okHttpClient = okHttpClient
19 | return this
20 | }
21 |
22 | fun gson(gson: Gson): ApiBuilder {
23 | this.gson = gson
24 | return this
25 | }
26 |
27 | fun callAdapter(callAdapter: CallAdapter.Factory): ApiBuilder {
28 | this.callAdapter = callAdapter
29 | return this
30 | }
31 |
32 | fun createApi(
33 | apiClass: KClass,
34 | baseUrl: String
35 | ): T {
36 | val builder = Retrofit.Builder()
37 | .baseUrl(baseUrl)
38 | callAdapter?.let {
39 | builder.addCallAdapterFactory(it)
40 | }
41 | okHttpClient?.let {
42 | builder.client(it)
43 | }
44 | gson.let {
45 | builder.addConverterFactory(GsonConverterFactory.create(gson))
46 | }
47 |
48 | return builder.build()
49 | .create(apiClass.java)
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/mvp/flightorder/FlightOrderView.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.flightorder
2 |
3 | import com.arellomobile.mvp.MvpView
4 | import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
5 | import com.arellomobile.mvp.viewstate.strategy.OneExecutionStateStrategy
6 | import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
7 | import org.joda.time.DateTime
8 |
9 | @StateStrategyType(AddToEndSingleStrategy::class)
10 | interface FlightOrderView : MvpView {
11 |
12 | fun showEmptyDepartCity()
13 |
14 | fun showDepartCity(departCity: String)
15 |
16 | @StateStrategyType(OneExecutionStateStrategy::class)
17 | fun showDepartCitySelector(cities: List)
18 |
19 | fun showEmptyArriveCity()
20 |
21 | fun showArriveCity(arriveCity: String)
22 |
23 | @StateStrategyType(OneExecutionStateStrategy::class)
24 | fun showArriveCitySelector(cities: List)
25 |
26 | fun enableSwapCitiesButton(isEnabled: Boolean)
27 |
28 | fun showDepartDate(departDate: DateTime)
29 |
30 | @StateStrategyType(OneExecutionStateStrategy::class)
31 | fun showDepartDatePicker(departDate: DateTime)
32 |
33 | fun showReturnDate(returnDate: DateTime)
34 |
35 | fun updateReturnDateVisibility(isVisible: Boolean)
36 |
37 | @StateStrategyType(OneExecutionStateStrategy::class)
38 | fun showReturnDatePicker(returnDate: DateTime)
39 |
40 | fun showPassengersData(passengersData: PassengersData)
41 |
42 | fun showValidationErrorMessage(errorMessage: String)
43 |
44 | fun showMessage(text: String)
45 |
46 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/UTairApplication.kt:
--------------------------------------------------------------------------------
1 | package com.utair
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.multidex.MultiDex
6 | import com.utair.data.global.network.ApiConstants
7 | import com.utair.di.*
8 | import com.utair.global.appinitializer.AppInitializer
9 | import toothpick.Toothpick
10 | import toothpick.configuration.Configuration
11 | import java.util.*
12 |
13 | class UTairApplication : Application() {
14 |
15 | val appLaunchCode = UUID.randomUUID().toString()
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 |
20 | initDi()
21 | getGlobal()
22 | }
23 |
24 | private fun initDi() {
25 | if (BuildConfig.DEBUG) {
26 | Toothpick.setConfiguration(Configuration.forDevelopment().preventMultipleRootScopes())
27 | } else {
28 | Toothpick.setConfiguration(Configuration.forProduction())
29 | }
30 |
31 | Toothpick.openScope(DI.APP_SCOPE)
32 | .installModules(
33 | AppModule(this),
34 | AppNavigationModule(),
35 | DomainModule(),
36 | DataModule(
37 | utairApiBaseUrl = ApiConstants.CITIES_BASE_URL,
38 | weatherApiBaseUrl = ApiConstants.OPEN_WEATHER_BASE_URL
39 | )
40 | )
41 | }
42 |
43 | override fun attachBaseContext(base: Context) {
44 | super.attachBaseContext(base)
45 | MultiDex.install(this)
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/mvp/flightorder/FlightOrderView.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.flightorder
2 |
3 | import com.arellomobile.mvp.MvpView
4 | import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
5 | import com.arellomobile.mvp.viewstate.strategy.OneExecutionStateStrategy
6 | import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
7 | import org.joda.time.DateTime
8 |
9 | @StateStrategyType(AddToEndSingleStrategy::class)
10 | interface FlightOrderView : MvpView {
11 |
12 | fun showEmptyDepartCity()
13 |
14 | fun showDepartCity(departCity: String)
15 |
16 | @StateStrategyType(OneExecutionStateStrategy::class)
17 | fun showDepartCitySelector(cities: List)
18 |
19 | fun showEmptyArriveCity()
20 |
21 | fun showArriveCity(arriveCity: String)
22 |
23 | @StateStrategyType(OneExecutionStateStrategy::class)
24 | fun showArriveCitySelector(cities: List)
25 |
26 | fun disableSwapCitiesButton()
27 |
28 | fun enableSwapCitiesButton()
29 |
30 | fun showDepartDate(departDate: DateTime)
31 |
32 | @StateStrategyType(OneExecutionStateStrategy::class)
33 | fun showDepartDatePicker(departDate: DateTime)
34 |
35 | fun showReturnDate(returnDate: DateTime)
36 |
37 | fun updateReturnDateVisibility(isVisible: Boolean)
38 |
39 | @StateStrategyType(OneExecutionStateStrategy::class)
40 | fun showReturnDatePicker(returnDate: DateTime)
41 |
42 | fun showPassengersData(passengersData: PassengersData)
43 |
44 | fun showValidationErrorMessage(errorMessage: String)
45 |
46 | fun showMessage(text: String)
47 |
48 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/DiExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | import com.utair.DI
4 | import toothpick.Scope
5 | import toothpick.Toothpick
6 | import toothpick.config.Binding
7 | import toothpick.config.Module
8 | import kotlin.reflect.KClass
9 |
10 | fun Any.objectScopeName() = "${javaClass.simpleName}_${hashCode()}"
11 |
12 | fun getAppScope(): Scope = Toothpick.openScope(DI.APP_SCOPE)
13 |
14 | inline fun Scope.get(): T = getInstance(T::class.java)
15 |
16 | inline fun getGlobal(): T = getAppScope().get()
17 |
18 | internal fun Module.bind(key: KClass): Binding {
19 | val binding = Binding(key.java)
20 | bindingSet.add(binding)
21 | return binding
22 | }
23 |
24 | internal fun Binding.to(kclass: KClass): Binding.BoundStateForClassBinding {
25 | return this.to(kclass.java)
26 | }
27 |
28 | fun Scope.installModule(moduleInstalling: Module.() -> Unit) {
29 | val module = object : Module() {}
30 | moduleInstalling(module)
31 | installModules(module)
32 | }
33 |
34 | inline fun Module.namedBind(value: T) {
35 | bind(T::class.java)
36 | .withName(Name::class.java)
37 | .toInstance(value)
38 | }
39 |
40 | fun Module.bindPrimitive(annotation: KClass, value: T) {
41 | bind(PrimitiveWrapper::class.java)
42 | .withName(annotation.java)
43 | .toInstance(PrimitiveWrapper(value))
44 | }
45 |
46 | fun Module.bindPrimitive(name: String, value: T) {
47 | bind(PrimitiveWrapper::class.java)
48 | .withName(name)
49 | .toInstance(PrimitiveWrapper(value))
50 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/mvp/weatherforecast/CityWeatherForecastPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp.weatherforecast
2 |
3 | import com.arellomobile.mvp.InjectViewState
4 | import com.utair.di.presenters.weatherforecast.cityforecast.CityName
5 | import com.utair.domain.global.exceptions.NoNetworkException
6 | import com.utair.domain.weatherforecast.WeatherForecastInteractor
7 | import com.utair.presentation.ui.global.base.mvp.BasePresenter
8 | import com.utair.presentation.mvp.global.ErrorHandler
9 | import io.reactivex.rxkotlin.subscribeBy
10 | import javax.inject.Inject
11 |
12 | @InjectViewState
13 | class CityWeatherForecastPresenter @Inject constructor(
14 | @CityName private val cityName: String,
15 | private val weatherForecastInteractor: WeatherForecastInteractor,
16 | private val errorHandler: ErrorHandler
17 | ) : BasePresenter() {
18 |
19 | override fun onFirstViewAttach() {
20 | super.onFirstViewAttach()
21 |
22 | showForecast()
23 | }
24 |
25 | private fun showForecast() {
26 | weatherForecastInteractor.getWeatherForecastForCity(cityName)
27 | .subscribeBy(
28 | onSuccess = {
29 | viewState.showForecast(it.dailyForecasts)
30 | },
31 | onError = { handleError(it) }
32 | )
33 | .connect()
34 | }
35 |
36 | private fun handleError(throwable: Throwable) {
37 | if (throwable is NoNetworkException) {
38 | viewState.showNoNetworkMessage()
39 | } else {
40 | errorHandler.handle(throwable)
41 | }
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/modules/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.modules
2 |
3 | import android.content.Context
4 | import com.utair.di.qualifiers.JobScheduler
5 | import com.utair.domain.global.ResourceManager
6 | import com.utair.global.appinitializer.AppInitializer
7 | import com.utair.global.appinitializer.AppInitializerElement
8 | import com.utair.global.appinitializer.AppInitializersProvider
9 | import com.utair.global.appinitializer.initialzers.AppConfigurationInitializer
10 | import com.utair.presentation.mvp.global.AndroidResourceManager
11 | import dagger.Module
12 | import dagger.Provides
13 | import io.reactivex.Scheduler
14 | import io.reactivex.schedulers.Schedulers
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | class ApplicationModule(
19 | private val context: Context
20 | ) {
21 |
22 | @Provides
23 | @Singleton
24 | fun provideApplicationContext(): Context = context
25 |
26 | @Provides
27 | @Singleton
28 | fun provideResourceManager(resourceManager: AndroidResourceManager): ResourceManager {
29 | return resourceManager
30 | }
31 |
32 | @Provides
33 | @Singleton
34 | @JobScheduler
35 | fun provideJobScheduler(): Scheduler {
36 | return Schedulers.computation()
37 | }
38 |
39 | @Provides
40 | @Singleton
41 | fun provideAppInitializer(
42 | context: Context,
43 | appConfigurationInitializer: AppConfigurationInitializer
44 | ): AppInitializer {
45 | val appInitializersProvider = object : AppInitializersProvider {
46 | override fun getInitializers(): List {
47 | return listOf(appConfigurationInitializer)
48 | }
49 | }
50 | return AppInitializer(context, appInitializersProvider)
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/WeatherForecastDeserializer.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network
2 |
3 | import kotlin.jvm.Throws
4 | import com.google.gson.JsonDeserializationContext
5 | import com.google.gson.JsonDeserializer
6 | import com.google.gson.JsonElement
7 | import com.google.gson.JsonParseException
8 | import com.utair.data.global.network.responses.WeatherForecastResponse
9 | import java.lang.reflect.Type
10 | import java.util.*
11 |
12 | class WeatherForecastDeserializer : JsonDeserializer {
13 |
14 | @Throws(JsonParseException::class)
15 | override fun deserialize(
16 | json: JsonElement,
17 | typeOfT: Type,
18 | context: JsonDeserializationContext
19 | ): WeatherForecastResponse {
20 | val jsonObject = json.asJsonObject
21 | val cityName = jsonObject.getAsJsonObject("city")["name"].asString
22 | val hourlyForecasts: MutableList = ArrayList()
23 | val hourlyForecastsJson = jsonObject.getAsJsonArray("list")
24 | for (hourlyForecastJsonElement in hourlyForecastsJson) {
25 | val hourlyForecastJson = hourlyForecastJsonElement.asJsonObject
26 | val timestamp = hourlyForecastJson["dt"].asLong //unix timestamp
27 | val weatherId = hourlyForecastJson.getAsJsonArray("weather")[0].asJsonObject["id"].asInt
28 | val temperature = hourlyForecastJson.getAsJsonObject("main")["temp"].asFloat
29 | val speed = hourlyForecastJson.getAsJsonObject("wind")["speed"].asFloat
30 | val hourlyForecast = WeatherForecastResponse.HourlyForecast(timestamp, weatherId, temperature, speed)
31 | hourlyForecasts.add(hourlyForecast)
32 | }
33 | return WeatherForecastResponse(cityName, hourlyForecasts)
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_fog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
19 |
23 |
27 |
31 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/global/base/BaseMvpActivity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import com.utair.UTairApplication.Companion.component
6 | import com.utair.presentation.ui.global.base.MvpAppCompatActivity
7 | import io.github.inflationx.viewpump.ViewPumpContextWrapper
8 | import me.aartikov.alligator.NavigationContext
9 | import me.aartikov.alligator.NavigationContextBinder
10 | import me.aartikov.alligator.R
11 | import me.aartikov.alligator.exceptions.ActivityResolvingException
12 | import me.aartikov.alligator.exceptions.NavigationException
13 |
14 | open class BaseMvpActivity : MvpAppCompatActivity() {
15 |
16 | protected val appComponent by lazy { component() }
17 | protected val navigationContextBinder by lazy {
18 | appComponent.getNavigationContextBinder()
19 | }
20 | protected val screenResolver by lazy {
21 | appComponent.getScreenResolver()
22 | }
23 |
24 | override fun attachBaseContext(newBase: Context) {
25 | super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase))
26 | }
27 |
28 | override fun onResumeFragments() {
29 | super.onResumeFragments()
30 |
31 | bindNavigationContext()
32 | }
33 |
34 | override fun onPause() {
35 | unbindNavigationContext()
36 |
37 | super.onPause()
38 | }
39 |
40 |
41 | //these methods are created to customize it in child classes if it needed
42 | protected open fun bindNavigationContext() {
43 | navigationContextBinder.bind(createNavigationContext())
44 | }
45 |
46 | protected open fun createNavigationContext(): NavigationContext {
47 | val factory = appComponent.getNavigationFactory()
48 | return NavigationContext.Builder(this, factory)
49 | .build()
50 | }
51 |
52 | protected open fun unbindNavigationContext() {
53 | navigationContextBinder.unbind(this)
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/weatherforecast/DailyForecastAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.LinearLayoutManager
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.utair.core.R
10 | import com.utair.domain.global.models.WeatherForecast.DailyForecast
11 | import com.utair.presentation.ui.weatherforecast.DailyForecastAdapter.DayForecastViewHolder
12 | import kotlinx.android.synthetic.main.item_daily_forecast.view.*
13 | import java.text.SimpleDateFormat
14 |
15 | class DailyForecastAdapter(
16 | private val context: Context,
17 | private val dailyForecasts: List
18 | ) : RecyclerView.Adapter() {
19 |
20 | private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
21 | private val dateFormatter = SimpleDateFormat("d MMM, E")
22 |
23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayForecastViewHolder {
24 | val view = layoutInflater.inflate(R.layout.item_daily_forecast, parent, false)
25 | val layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
26 | view.hourlyForecastList.layoutManager = layoutManager
27 | return DayForecastViewHolder(view)
28 | }
29 |
30 | override fun onBindViewHolder(holder: DayForecastViewHolder, position: Int) {
31 | val dailyForecast = dailyForecasts[position]
32 | val itemView = holder.itemView
33 | itemView.dayLabel.text = dateFormatter.format(dailyForecast.date.toDate())
34 | itemView.hourlyForecastList.adapter = HourlyForecastAdapter(context, dailyForecast.getHourlyForecasts())
35 | }
36 |
37 | override fun getItemCount() = dailyForecasts.size
38 |
39 | class DayForecastViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
40 |
41 | }
--------------------------------------------------------------------------------
/core/src/main/res/layout/toolbar_white.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
15 |
16 |
27 |
28 |
29 |
35 |
36 |
46 |
47 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app-rxjava/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | id("kotlin-android-extensions")
5 | id("kotlin-kapt")
6 | }
7 |
8 | android {
9 | compileSdkVersion(Build.Versions.compileSdk)
10 | buildToolsVersion(Build.Versions.buildTools)
11 |
12 | defaultConfig {
13 | applicationId = "com.utair"
14 | minSdkVersion(Build.Versions.minSdk)
15 | targetSdkVersion(Build.Versions.targetSdk)
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables.useSupportLibrary = true
21 | multiDexEnabled = true
22 | }
23 |
24 | buildFeatures.viewBinding = true
25 |
26 | buildTypes {
27 | getByName("release") {
28 | isMinifyEnabled = false
29 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
30 | }
31 | }
32 |
33 | compileOptions {
34 | sourceCompatibility = JavaVersion.VERSION_1_8
35 | targetCompatibility = JavaVersion.VERSION_1_8
36 | }
37 | }
38 |
39 | dependencies {
40 | api(project(":core"))
41 |
42 | kapt("com.arello-mobile:moxy-compiler:1.5.5")
43 | kapt("com.google.dagger:dagger-compiler:2.29.1")
44 | implementation("com.google.dagger:dagger:2.29.1")
45 |
46 | implementation("io.reactivex.rxjava2:rxjava:2.2.2")
47 | implementation("io.reactivex.rxjava2:rxandroid:2.1.0")
48 | implementation("io.reactivex.rxjava2:rxkotlin:2.0.0")
49 | implementation("com.squareup.retrofit2:adapter-rxjava2:2.3.0")
50 |
51 | // unit-tests
52 | testImplementation(Dependencies.Tests.kotlinReflect)
53 | testImplementation(kotlin(Dependencies.Tests.stdJdk))
54 |
55 | testImplementation(Dependencies.Tests.spek)
56 | testImplementation(Dependencies.Tests.spekRunner)
57 | testImplementation(Dependencies.Tests.mockk)
58 | testImplementation(Dependencies.Tests.strikt)
59 |
60 | testImplementation(Dependencies.Tests.okhttpMockServer)
61 | }
62 |
--------------------------------------------------------------------------------
/core/src/main/res/layout/view_passenger_counter.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
20 |
21 |
26 |
27 |
33 |
34 |
38 |
39 |
40 |
46 |
47 |
48 |
52 |
--------------------------------------------------------------------------------
/core/src/test/java/com/utair/domain/flightorder/FlightOrderDataValidatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.flightorder
2 |
3 | import com.utair.core.R
4 | import com.utair.domain.global.ResourceManager
5 | import com.utair.domain.global.exceptions.FlightOrderDataValidationError
6 | import com.utair.domain.global.models.FlightOrderData
7 | import io.mockk.every
8 | import io.mockk.mockk
9 | import org.spekframework.spek2.Spek
10 | import org.spekframework.spek2.lifecycle.CachingMode
11 | import org.spekframework.spek2.style.specification.describe
12 | import strikt.api.expectThrows
13 | import strikt.assertions.isEqualTo
14 | import strikt.assertions.message
15 |
16 | class FlightOrderDataValidatorTest : Spek({
17 |
18 | val resourceManager by memoized { mockk() }
19 | val validator by memoized(CachingMode.TEST) {
20 | FlightOrderDataValidator(resourceManager)
21 | }
22 |
23 | describe("on depart city is not selected") {
24 | val errorMessage = "Depart city not selected"
25 | val data = FlightOrderData(null, "London")
26 | beforeEachTest {
27 | every {
28 | resourceManager.getString(R.string.depart_city_is_empty_message)
29 | } returns errorMessage
30 | }
31 | it("should throw 'arrive city not selected' error") {
32 | expectThrows {
33 | validator.validate(data)
34 | }.message.isEqualTo(errorMessage)
35 | }
36 | }
37 |
38 | describe("on arrive city is not selected") {
39 | val errorMessage = "Arrive city not selected"
40 | val data = FlightOrderData("London", null)
41 | beforeEachTest {
42 | every {
43 | resourceManager.getString(R.string.arrive_city_is_empty_message)
44 | } returns errorMessage
45 | }
46 | it("should throw 'arrive city not selected' error") {
47 | expectThrows {
48 | validator.validate(data)
49 | }.message.isEqualTo(errorMessage)
50 | }
51 | }
52 |
53 | })
54 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/di/modules/DataModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di.modules
2 |
3 | import com.google.gson.GsonBuilder
4 | import com.utair.data.global.network.*
5 | import com.utair.data.global.network.responses.WeatherForecastResponse
6 | import dagger.Module
7 | import dagger.Provides
8 | import okhttp3.OkHttpClient
9 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | class DataModule(
14 | private val utairApiBaseUrl: String,
15 | private val weatherApiBaseUrl: String
16 | ) {
17 |
18 | @Provides
19 | @Singleton
20 | fun provideUTairApi(networkCheckInterceptor: NetworkCheckInterceptor): UTairApiService {
21 | val okHttpClient = OkHttpClient.Builder()
22 | .addInterceptor(networkCheckInterceptor)
23 | .addInterceptor(WeatherApiInterceptor())
24 | .build()
25 |
26 | return newApiBuilder()
27 | .okHttpClient(okHttpClient)
28 | .createApi(
29 | apiClass = UTairApiService::class,
30 | baseUrl = utairApiBaseUrl
31 | )
32 | }
33 |
34 | @Provides
35 | @Singleton
36 | fun provideWeatherApi(networkCheckInterceptor: NetworkCheckInterceptor): WeatherApiService {
37 | val okHttpClient = OkHttpClient.Builder()
38 | .addInterceptor(networkCheckInterceptor)
39 | .addInterceptor(WeatherApiInterceptor())
40 | .build()
41 |
42 | val gson = GsonBuilder()
43 | .registerTypeAdapter(WeatherForecastResponse::class.java, WeatherForecastDeserializer())
44 | .create()
45 | return newApiBuilder()
46 | .okHttpClient(okHttpClient)
47 | .gson(gson)
48 | .createApi(
49 | apiClass = WeatherApiService::class,
50 | baseUrl = weatherApiBaseUrl
51 | )
52 | }
53 |
54 | private fun newApiBuilder() = ApiBuilder()
55 | .callAdapter(RxJava2CallAdapterFactory.create())
56 |
57 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_question.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
27 |
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/UTairApplication.kt:
--------------------------------------------------------------------------------
1 | package com.utair
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.appcompat.app.AppCompatDelegate
6 | import androidx.multidex.MultiDex
7 | import com.utair.data.global.network.ApiConstants
8 | import com.utair.di.ApplicationComponent
9 | import com.utair.di.DaggerApplicationComponent
10 | import com.utair.di.modules.ApplicationModule
11 | import com.utair.di.modules.DataModule
12 | import com.utair.di.modules.NavigationModule
13 | import io.github.inflationx.calligraphy3.CalligraphyConfig
14 | import io.github.inflationx.calligraphy3.CalligraphyInterceptor
15 | import io.github.inflationx.viewpump.ViewPump
16 |
17 | class UTairApplication : Application() {
18 |
19 | override fun onCreate() {
20 | super.onCreate()
21 |
22 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
23 |
24 | val calligraphyInterceptor = CalligraphyInterceptor(CalligraphyConfig.Builder().build())
25 | val viewPumpConfig = ViewPump.builder()
26 | .addInterceptor(calligraphyInterceptor)
27 | .build()
28 | ViewPump.init(viewPumpConfig)
29 |
30 | applicationComponent = prepareComponent()
31 | }
32 |
33 | private fun prepareComponent(): ApplicationComponent {
34 | return DaggerApplicationComponent.builder()
35 | .dataModule(DataModule(
36 | utairApiBaseUrl = ApiConstants.CITIES_BASE_URL,
37 | weatherApiBaseUrl = ApiConstants.OPEN_WEATHER_BASE_URL
38 | ))
39 | .applicationModule(ApplicationModule(applicationContext))
40 | .navigationModule(NavigationModule())
41 | .build()
42 | }
43 |
44 | override fun attachBaseContext(base: Context) {
45 | super.attachBaseContext(base)
46 | MultiDex.install(this)
47 | }
48 |
49 | companion object {
50 |
51 | private lateinit var applicationComponent: ApplicationComponent
52 |
53 | @JvmStatic
54 | fun component(): ApplicationComponent {
55 | return applicationComponent
56 | }
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/flightorder/PassengersCountPicker.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.flightorder
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.widget.LinearLayout
6 | import com.utair.core.R
7 | import com.utair.presentation.mvp.flightorder.PassengersData
8 |
9 | class PassengersCountPicker @JvmOverloads constructor(
10 | context: Context,
11 | attrs: AttributeSet?,
12 | theme: Int = 0
13 | ) : LinearLayout(context, attrs, theme) {
14 |
15 | private val adultsCounter by lazy { PassengerCounter(context) }
16 | private val kidsCounter by lazy { PassengerCounter(context) }
17 | private val babiesCounter by lazy { PassengerCounter(context) }
18 | var onValueChangedListener: ((PassengersData) -> Unit)? = null
19 |
20 | init {
21 | orientation = HORIZONTAL
22 |
23 | addView(adultsCounter)
24 | addView(kidsCounter)
25 | addView(babiesCounter)
26 |
27 | adultsCounter.setIcon(R.drawable.ic_passenger_adult)
28 | adultsCounter.setText(R.string.adult_passenger)
29 | adultsCounter.minValue = 1
30 | adultsCounter.value = 1
31 | adultsCounter.onValueChanged = { onValueChanged() }
32 |
33 | kidsCounter.setIcon(R.drawable.ic_passenger_kid)
34 | kidsCounter.setText(R.string.kid_passenger)
35 | kidsCounter.value = 0
36 | kidsCounter.onValueChanged = { onValueChanged() }
37 |
38 | babiesCounter.setIcon(R.drawable.ic_passenger_baby)
39 | babiesCounter.setText(R.string.baby_passenger)
40 | babiesCounter.value = 0
41 | babiesCounter.onValueChanged = { onValueChanged() }
42 | }
43 |
44 | fun setValues(passengersData: PassengersData) {
45 | adultsCounter.value = passengersData.adultCount
46 | kidsCounter.value = passengersData.kidCount
47 | babiesCounter.value = passengersData.babyCount
48 | }
49 |
50 | private fun onValueChanged() {
51 | val passengersData = PassengersData(
52 | adultCount = adultsCounter.value,
53 | kidCount = kidsCounter.value,
54 | babyCount = babiesCounter.value
55 | )
56 | onValueChangedListener?.invoke(passengersData)
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/global/base/MvpAppCompatDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatDialogFragment
5 | import com.arellomobile.mvp.MvpDelegate
6 |
7 | /**
8 | * Date: 17-Dec-16
9 | * Time: 21:55
10 | *
11 | * @author Konstantin Tckhovrebov
12 | */
13 | class MvpAppCompatDialogFragment : AppCompatDialogFragment() {
14 |
15 | private var mIsStateSaved = false
16 | private val mvpDelegate by lazy {
17 | MvpDelegate(this)
18 | }
19 |
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | mvpDelegate.onCreate(savedInstanceState)
23 | }
24 |
25 | override fun onResume() {
26 | super.onResume()
27 | mIsStateSaved = false
28 | mvpDelegate.onAttach()
29 | }
30 |
31 | override fun onSaveInstanceState(outState: Bundle) {
32 | super.onSaveInstanceState(outState)
33 | mIsStateSaved = true
34 | mvpDelegate.onSaveInstanceState(outState)
35 | mvpDelegate.onDetach()
36 | }
37 |
38 | override fun onStop() {
39 | super.onStop()
40 | mvpDelegate.onDetach()
41 | }
42 |
43 | override fun onDestroyView() {
44 | super.onDestroyView()
45 | mvpDelegate.onDetach()
46 | mvpDelegate.onDestroyView()
47 | }
48 |
49 | override fun onDestroy() {
50 | super.onDestroy()
51 |
52 | //We leave the screen and respectively all fragments will be destroyed
53 | if (activity!!.isFinishing) {
54 | mvpDelegate.onDestroy()
55 | return
56 | }
57 |
58 | // When we rotate device isRemoving() return true for fragment placed in backstack
59 | // http://stackoverflow.com/questions/34649126/fragment-back-stack-and-isremoving
60 | if (mIsStateSaved) {
61 | mIsStateSaved = false
62 | return
63 | }
64 |
65 | // See https://github.com/Arello-Mobile/Moxy/issues/24
66 | var anyParentIsRemoving = false
67 | var parent = parentFragment
68 | while (!anyParentIsRemoving && parent != null) {
69 | anyParentIsRemoving = parent.isRemoving
70 | parent = parent.parentFragment
71 | }
72 | if (isRemoving || anyParentIsRemoving) {
73 | mvpDelegate.onDestroy()
74 | }
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/ui/weatherforecast/CityWeatherForecastFragment.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.Toast
8 | import com.arellomobile.mvp.presenter.InjectPresenter
9 | import com.arellomobile.mvp.presenter.ProvidePresenter
10 | import com.utair.R
11 | import com.utair.core.databinding.FragmentCityWeatherForecastBinding
12 | import com.utair.di.bindPrimitive
13 | import com.utair.di.get
14 | import com.utair.di.installModule
15 | import com.utair.di.qualifiers.CityName
16 | import com.utair.domain.global.models.WeatherForecast.DailyForecast
17 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastPresenter
18 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastView
19 | import com.utair.presentation.ui.global.base.BaseFragment
20 | import toothpick.Scope
21 |
22 | class CityWeatherForecastFragment : BaseFragment(), CityWeatherForecastView {
23 |
24 | override val scopeModuleInstaller: (Scope) -> Unit = {
25 | it.installModule {
26 | val cityName = arguments!!.getString(CITY_NAME_ARG)!!
27 | bindPrimitive(CityName::class, cityName)
28 | }
29 | }
30 |
31 | private val binding by lazy {
32 | FragmentCityWeatherForecastBinding.inflate(layoutInflater)
33 | }
34 |
35 | @InjectPresenter
36 | lateinit var presenter: CityWeatherForecastPresenter
37 |
38 | @ProvidePresenter
39 | fun providePresenter() = scope.get()
40 |
41 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
42 | return binding.root
43 | }
44 |
45 | override fun showForecast(dailyForecasts: List) {
46 | binding.forecastList.adapter = DailyForecastAdapter(context!!, dailyForecasts)
47 | }
48 |
49 | override fun showNoNetworkMessage() {
50 | Toast.makeText(context, getString(R.string.no_network_message), Toast.LENGTH_SHORT).show()
51 | }
52 |
53 | companion object {
54 |
55 | const val CITY_NAME_ARG = "city_name"
56 |
57 | fun newInstance(cityName: String): CityWeatherForecastFragment {
58 | val fragment = CityWeatherForecastFragment()
59 | fragment.arguments = Bundle().apply {
60 | putString(CITY_NAME_ARG, cityName)
61 | }
62 | return fragment
63 | }
64 |
65 | }
66 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/flightorder/PassengerCounter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.flightorder
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.LayoutInflater
6 | import android.widget.LinearLayout
7 | import androidx.annotation.DrawableRes
8 | import androidx.annotation.StringRes
9 | import com.utair.core.R
10 | import kotlinx.android.synthetic.main.view_passenger_counter.view.*
11 |
12 | class PassengerCounter @JvmOverloads constructor(
13 | context: Context,
14 | attrs: AttributeSet? = null,
15 | theme: Int = 0
16 | ) : LinearLayout(context, attrs, theme) {
17 |
18 | var value: Int = 0
19 | set(value) {
20 | field = value
21 | counterValue.text = value.toString()
22 | }
23 | var minValue: Int = 0
24 | var maxValue: Int = Int.MAX_VALUE
25 | private val isMaxValueReached: Boolean
26 | get() = value == maxValue
27 | var onValueChanged: ((value: Int) -> Unit)? = null
28 |
29 | init {
30 | orientation = VERTICAL
31 | val inflater = LayoutInflater.from(context)
32 | inflater.inflate(R.layout.view_passenger_counter, this, true)
33 |
34 | incrementButton.setOnClickListener { incrementValue() }
35 | decrementButton.setOnClickListener { decrementValue() }
36 | }
37 |
38 | fun setIcon(@DrawableRes iconResId: Int) {
39 | counterIcon.setImageResource(iconResId)
40 | }
41 |
42 | fun setText(@StringRes textResId: Int) {
43 | counterLabel.setText(textResId)
44 | }
45 |
46 | private fun incrementValue() {
47 | if (!isMaxValueReached) {
48 | ++value
49 | animateValueView()
50 | updateViewsState()
51 | onValueChanged?.invoke(value)
52 | }
53 | }
54 |
55 | private fun decrementValue() {
56 | if (value > minValue) {
57 | --value
58 | animateValueView()
59 | updateViewsState()
60 | onValueChanged?.invoke(value)
61 | }
62 | }
63 |
64 | private fun updateViewsState() {
65 | val isEnabled = value != 0
66 | counterIcon.isEnabled = isEnabled
67 | counterValue.isEnabled = isEnabled
68 | }
69 |
70 | private fun animateValueView() {
71 | counterValue.animate()
72 | .scaleX(1.3f)
73 | .scaleY(1.3f)
74 | .setDuration(150)
75 | .withEndAction {
76 | counterValue.animate().scaleX(1f).scaleY(1f).duration = 50
77 | }
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/global/base/MvpAppCompatFragment.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import com.arellomobile.mvp.MvpDelegate
6 |
7 | /**
8 | * Date: 19-Dec-15
9 | * Time: 13:25
10 | *
11 | * @author Alexander Blinov
12 | * @author Yuri Shmakov
13 | * @author Konstantin Tckhovrebov
14 | */
15 | open class MvpAppCompatFragment : Fragment() {
16 |
17 | private var mIsStateSaved = false
18 | private val mvpDelegate by lazy {
19 | MvpDelegate(this)
20 | }
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | mvpDelegate.onCreate(savedInstanceState)
25 | }
26 |
27 | override fun onStart() {
28 | super.onStart()
29 | mIsStateSaved = false
30 | mvpDelegate.onAttach()
31 | }
32 |
33 | override fun onResume() {
34 | super.onResume()
35 | mIsStateSaved = false
36 | mvpDelegate.onAttach()
37 | }
38 |
39 | override fun onSaveInstanceState(outState: Bundle) {
40 | super.onSaveInstanceState(outState)
41 | mIsStateSaved = true
42 | mvpDelegate.onSaveInstanceState(outState)
43 | mvpDelegate.onDetach()
44 | }
45 |
46 | override fun onStop() {
47 | super.onStop()
48 | mvpDelegate.onDetach()
49 | }
50 |
51 | override fun onDestroyView() {
52 | super.onDestroyView()
53 | mvpDelegate.onDetach()
54 | mvpDelegate.onDestroyView()
55 | }
56 |
57 | override fun onDestroy() {
58 | super.onDestroy()
59 |
60 | //We leave the screen and respectively all fragments will be destroyed
61 | if (activity!!.isFinishing) {
62 | mvpDelegate.onDestroy()
63 | return
64 | }
65 |
66 | // When we rotate device isRemoving() return true for fragment placed in backstack
67 | // http://stackoverflow.com/questions/34649126/fragment-back-stack-and-isremoving
68 | if (mIsStateSaved) {
69 | mIsStateSaved = false
70 | return
71 | }
72 |
73 | // See https://github.com/Arello-Mobile/Moxy/issues/24
74 | var anyParentIsRemoving = false
75 | var parent = parentFragment
76 | while (!anyParentIsRemoving && parent != null) {
77 | anyParentIsRemoving = parent.isRemoving
78 | parent = parent.parentFragment
79 | }
80 | if (isRemoving || anyParentIsRemoving) {
81 | mvpDelegate.onDestroy()
82 | }
83 | }
84 |
85 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/weatherforecast/CityWeatherForecastFragment.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.Toast
8 | import com.arellomobile.mvp.presenter.InjectPresenter
9 | import com.arellomobile.mvp.presenter.ProvidePresenter
10 | import com.utair.R
11 | import com.utair.core.databinding.FragmentCityWeatherForecastBinding
12 | import com.utair.di.presenters.weatherforecast.cityforecast.CityForecastPresenterModule
13 | import com.utair.di.presenters.weatherforecast.cityforecast.DaggerCityForecastComponent
14 | import com.utair.domain.global.models.WeatherForecast.DailyForecast
15 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastPresenter
16 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastView
17 | import com.utair.presentation.ui.global.base.BaseFragment
18 |
19 | class CityWeatherForecastFragment : BaseFragment(), CityWeatherForecastView {
20 |
21 | private val binding by lazy {
22 | FragmentCityWeatherForecastBinding.inflate(layoutInflater)
23 | }
24 |
25 | @InjectPresenter
26 | lateinit var presenter: CityWeatherForecastPresenter
27 |
28 | @ProvidePresenter
29 | fun providePresenter(): CityWeatherForecastPresenter {
30 | val cityName = arguments!!.getString(CITY_NAME_ARG)!!
31 | return DaggerCityForecastComponent.builder()
32 | .applicationComponent(appComponent)
33 | .cityForecastPresenterModule(CityForecastPresenterModule(cityName))
34 | .build()
35 | .getCityWeatherForecastPresenter()
36 | }
37 |
38 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
39 | return binding.root
40 | }
41 |
42 | override fun showForecast(dailyForecasts: List) {
43 | binding.forecastList.adapter = DailyForecastAdapter(context!!, dailyForecasts)
44 | }
45 |
46 | override fun showNoNetworkMessage() {
47 | Toast.makeText(context, getString(R.string.no_network_message), Toast.LENGTH_SHORT).show()
48 | }
49 |
50 | companion object {
51 |
52 | const val CITY_NAME_ARG = "city_name"
53 |
54 | fun newInstance(cityName: String): CityWeatherForecastFragment {
55 | val fragment = CityWeatherForecastFragment()
56 | fragment.arguments = Bundle().apply {
57 | putString(CITY_NAME_ARG, cityName)
58 | }
59 | return fragment
60 | }
61 |
62 | }
63 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app-coroutines/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import de.mannodermaus.gradle.plugins.junit5.junitPlatform
2 |
3 | plugins {
4 | id("com.android.application")
5 | id("kotlin-android")
6 | id("kotlin-android-extensions")
7 | id("kotlin-kapt")
8 | id("de.mannodermaus.android-junit5")
9 | }
10 |
11 | android {
12 | compileSdkVersion(Build.Versions.compileSdk)
13 | buildToolsVersion(Build.Versions.buildTools)
14 |
15 | defaultConfig {
16 | applicationId = "com.utair"
17 | minSdkVersion(Build.Versions.minSdk)
18 | targetSdkVersion(Build.Versions.targetSdk)
19 | versionCode = 1
20 | versionName = "1.0"
21 |
22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
23 | vectorDrawables.useSupportLibrary = true
24 | multiDexEnabled = true
25 | }
26 |
27 | buildFeatures.viewBinding = true
28 |
29 | buildTypes {
30 | getByName("release") {
31 | isMinifyEnabled = false
32 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
33 | }
34 | }
35 |
36 | compileOptions {
37 | sourceCompatibility = JavaVersion.VERSION_1_8
38 | targetCompatibility = JavaVersion.VERSION_1_8
39 | }
40 | }
41 |
42 | dependencies {
43 | val kotlinCoroutineVersion = "1.3.3"
44 | val toothpickVersion = "2.0.0"
45 |
46 | api(project(":core"))
47 |
48 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutineVersion")
49 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutineVersion")
50 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutineVersion")
51 |
52 | kapt("com.arello-mobile:moxy-compiler:1.5.5")
53 | implementation("com.github.stephanenicolas.toothpick:toothpick:$toothpickVersion")
54 | implementation("com.github.stephanenicolas.toothpick:toothpick-runtime:$toothpickVersion")
55 | kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:$toothpickVersion")
56 |
57 | // unit-tests
58 | testImplementation(Dependencies.Tests.kotlinReflect)
59 | testImplementation(kotlin(Dependencies.Tests.stdJdk))
60 |
61 | testImplementation(Dependencies.Tests.spek)
62 | testImplementation(Dependencies.Tests.spekRunner)
63 | testImplementation(Dependencies.Tests.mockk)
64 | testImplementation(Dependencies.Tests.strikt)
65 |
66 | testImplementation(Dependencies.Tests.okhttpMockServer)
67 |
68 | androidTestImplementation(Dependencies.Tests.junit)
69 | androidTestImplementation(Dependencies.Tests.rules)
70 | androidTestImplementation(Dependencies.Tests.kakao)
71 | androidTestImplementation(Dependencies.Tests.kaspresso)
72 | }
73 |
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream
2 | import java.util.*
3 |
4 | plugins {
5 | id("com.android.library")
6 | id("kotlin-android")
7 | id("kotlin-android-extensions")
8 | id("kotlin-kapt")
9 | }
10 |
11 | android {
12 | compileSdkVersion(Build.Versions.compileSdk)
13 | buildToolsVersion(Build.Versions.buildTools)
14 |
15 | defaultConfig {
16 | minSdkVersion(Build.Versions.minSdk)
17 | targetSdkVersion(Build.Versions.targetSdk)
18 |
19 | val properties = Properties()
20 | val propsFile = project.rootProject.file("api_keys.properties")
21 | properties.load(FileInputStream(propsFile))
22 | val openWeatherApiKey = properties.getProperty("open_weather_api_key")
23 | buildConfigField("String", "OPEN_WEATHER_API_KEY", "\"$openWeatherApiKey\"")
24 |
25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
26 | vectorDrawables.useSupportLibrary = true
27 | multiDexEnabled = true
28 | }
29 |
30 | buildFeatures.viewBinding = true
31 |
32 | buildTypes {
33 | getByName("release") {
34 | isMinifyEnabled = false
35 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
36 | }
37 | }
38 |
39 | compileOptions {
40 | sourceCompatibility = JavaVersion.VERSION_1_8
41 | targetCompatibility = JavaVersion.VERSION_1_8
42 | }
43 | kotlinOptions {
44 | jvmTarget = JavaVersion.VERSION_1_8.toString()
45 | }
46 | }
47 |
48 | dependencies {
49 | api("com.android.support:multidex:1.0.3")
50 |
51 | api("androidx.appcompat:appcompat:1.2.0")
52 | api("com.google.android.material:material:1.2.1")
53 | api("androidx.recyclerview:recyclerview:1.1.0")
54 | api("javax.inject:javax.inject:1")
55 |
56 | api("com.github.aartikov.Alligator:alligator:4.0.0")
57 | api("com.arello-mobile:moxy:1.5.5")
58 | api("com.arello-mobile:moxy-app-compat:1.5.5")
59 |
60 | api("com.squareup.retrofit2:retrofit:2.9.0")
61 | api("com.squareup.retrofit2:converter-gson:2.9.0")
62 | api("com.squareup.okhttp3:logging-interceptor:${Dependencies.Versions.okhttpVersion}")
63 |
64 | api("io.github.inflationx:calligraphy3:3.1.1")
65 | api("io.github.inflationx:viewpump:2.0.3")
66 |
67 | api("com.wdullaer:materialdatetimepicker:4.2.3")
68 | api("com.afollestad.material-dialogs:core:3.3.0")
69 | api("joda-time:joda-time:2.10.6")
70 |
71 | api("com.jakewharton.timber:timber:4.7.1")
72 |
73 | testImplementation(Dependencies.Tests.spek)
74 | testImplementation(Dependencies.Tests.spekRunner)
75 | testImplementation(Dependencies.Tests.mockk)
76 | testImplementation(Dependencies.Tests.strikt)
77 | }
78 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/di/DataModule.kt:
--------------------------------------------------------------------------------
1 | package com.utair.di
2 |
3 | import com.google.gson.GsonBuilder
4 | import com.utair.data.global.UTairPreferences
5 | import com.utair.data.global.network.*
6 | import com.utair.data.global.network.mappers.WeatherForecastResponseMapper
7 | import com.utair.data.global.network.responses.WeatherForecastResponse
8 | import okhttp3.OkHttpClient
9 | import toothpick.config.Module
10 |
11 | class DataModule(
12 | private val utairApiBaseUrl: String,
13 | private val weatherApiBaseUrl: String
14 | ) : Module() {
15 |
16 | init {
17 | bind(UTairPreferences::class)
18 | .toProviderInstance { UTairPreferences(getGlobal()) }
19 |
20 | bind(NetworkCheckInterceptor::class)
21 | .toProviderInstance { NetworkCheckInterceptor(getGlobal()) }
22 |
23 | bind(NetworkChecker::class)
24 | .toProviderInstance { NetworkChecker(getGlobal()) }
25 |
26 | bind(WeatherForecastResponseMapper::class)
27 | .toProviderInstance { WeatherForecastResponseMapper() }
28 |
29 | bind(UTairApiService::class)
30 | .toProviderInstance { createUTairApi(getGlobal()) }
31 |
32 | bind(WeatherApiService::class)
33 | .toProviderInstance { createWeatherApi(getGlobal()) }
34 | }
35 |
36 | private fun createUTairApi(
37 | networkCheckInterceptor: NetworkCheckInterceptor
38 | ): UTairApiService {
39 | val okHttpClient = OkHttpClient.Builder()
40 | .addInterceptor(networkCheckInterceptor)
41 | .addInterceptor(WeatherApiInterceptor())
42 | .build()
43 |
44 | return ApiBuilder()
45 | .okHttpClient(okHttpClient)
46 | .createApi(
47 | apiClass = UTairApiService::class,
48 | baseUrl = utairApiBaseUrl
49 | )
50 | }
51 |
52 | private fun createWeatherApi(
53 | networkCheckInterceptor: NetworkCheckInterceptor
54 | ): WeatherApiService {
55 | val okHttpClient = OkHttpClient.Builder()
56 | .addInterceptor(networkCheckInterceptor)
57 | .addInterceptor(WeatherApiInterceptor())
58 | .build()
59 |
60 | val gson = GsonBuilder()
61 | .registerTypeAdapter(WeatherForecastResponse::class.java, WeatherForecastDeserializer())
62 | .create()
63 | return ApiBuilder()
64 | .okHttpClient(okHttpClient)
65 | .gson(gson)
66 | .createApi(
67 | apiClass = WeatherApiService::class,
68 | baseUrl = weatherApiBaseUrl
69 | )
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_snowflake.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
18 |
23 |
28 |
42 |
--------------------------------------------------------------------------------
/core/src/test/java/com/utair/domain/flightorder/PassengersDataValidatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.utair.domain.flightorder
2 |
3 | import com.utair.core.R
4 | import com.utair.domain.global.ResourceManager
5 | import com.utair.domain.global.exceptions.PassengersDataValidationError
6 | import com.utair.presentation.mvp.flightorder.PassengersData
7 | import io.mockk.every
8 | import io.mockk.mockk
9 | import org.spekframework.spek2.Spek
10 | import org.spekframework.spek2.lifecycle.CachingMode
11 | import org.spekframework.spek2.style.specification.describe
12 | import strikt.api.expectThrows
13 | import strikt.assertions.isEqualTo
14 | import strikt.assertions.message
15 |
16 | class PassengersDataValidatorTest : Spek({
17 |
18 | val resourceManager by memoized { mockk() }
19 | val validator by memoized(CachingMode.TEST) {
20 | PassengersDataValidator(resourceManager)
21 | }
22 |
23 | describe("on passengers count is correct") {
24 | val data = listOf(
25 | PassengersData(1, 0, 0),
26 | PassengersData(9, 0, 0),
27 | PassengersData(3, 3, 3)
28 | )
29 | val maxPassengerCountMessage = "Passengers count is too much"
30 | beforeEachTest {
31 | every {
32 | resourceManager.getString(R.string.max_passenger_count_message)
33 | } returns maxPassengerCountMessage
34 | }
35 | data.forEach {
36 | describe("a=${it.adultCount},k=${it.kidCount},b=${it.babyCount}") {
37 | it("should not throw any error") {
38 | validator.validate(it)
39 | }
40 | }
41 | }
42 | }
43 |
44 | describe("passengers number exceeds the maximum allowed") {
45 | val data = listOf(
46 | PassengersData(10, 0, 0),
47 | PassengersData(4, 3, 3),
48 | PassengersData(2, 3, 5)
49 | )
50 | val maxPassengerCountMessage = "Passengers count is too much"
51 | beforeEachTest {
52 | every {
53 | resourceManager.getString(R.string.max_passenger_count_message, any())
54 | } returns maxPassengerCountMessage
55 | }
56 | data.forEach {
57 |
58 | }
59 | }
60 |
61 | describe("babies count more than adults") {
62 | val data = listOf(
63 | PassengersData(1, 0, 2),
64 | PassengersData(2, 3, 4)
65 | )
66 | val babiesLessThanAdultsMessage = "Babies less than adults"
67 | beforeEachTest {
68 | every {
69 | resourceManager.getString(R.string.babies_less_adults_message)
70 | } returns babiesLessThanAdultsMessage
71 | }
72 | data.forEach {
73 | describe("a=${it.adultCount},k=${it.kidCount},b=${it.babyCount}") {
74 | it("should throw error") {
75 | expectThrows {
76 | validator.validate(it)
77 | }.message.isEqualTo(babiesLessThanAdultsMessage)
78 | }
79 | }
80 | }
81 | }
82 |
83 | })
84 |
--------------------------------------------------------------------------------
/app-coroutines/src/test/java/com/utair/presentation/mvp/WeatherForecastPresenterTest.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp
2 |
3 | import com.utair.di.PrimitiveWrapper
4 | import com.utair.domain.flightorder.MainInteractor
5 | import com.utair.domain.global.models.FlightOrderData
6 | import com.utair.presentation.mvp.global.ErrorHandler
7 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter
8 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastView
9 | import io.mockk.every
10 | import io.mockk.mockk
11 | import io.mockk.verifyOrder
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.test.resetMain
14 | import kotlinx.coroutines.test.setMain
15 | import me.aartikov.alligator.Navigator
16 | import org.spekframework.spek2.Spek
17 | import org.spekframework.spek2.lifecycle.CachingMode
18 | import org.spekframework.spek2.style.specification.describe
19 |
20 | class WeatherForecastPresenterTest : Spek({
21 |
22 | val departCity = "Moscow"
23 | val departCityWrapper by memoized { mockk>() }
24 | val arriveCity = "St. Petersburg"
25 | val arriveCityWrapper by memoized { mockk>() }
26 |
27 | val view by memoized { mockk(relaxed = true) }
28 |
29 | val presenter by memoized(CachingMode.TEST) {
30 | WeatherForecastPresenter(
31 | departCity = departCityWrapper,
32 | arriveCity = arriveCityWrapper
33 | )
34 | }
35 |
36 | beforeEachTest {
37 | Dispatchers.setMain(Dispatchers.Unconfined)
38 |
39 | every { departCityWrapper.value } returns departCity
40 | every { arriveCityWrapper.value } returns arriveCity
41 | }
42 |
43 | afterEachTest {
44 | Dispatchers.resetMain()
45 | }
46 |
47 | describe("start actions") {
48 | beforeEachTest {
49 | presenter.attachView(view)
50 | }
51 |
52 | it("should open tab depart tab") {
53 | verifyOrder {
54 | view.showForecastForCities(departCity, arriveCity)
55 | view.openForecastPage(WeatherForecastPresenter.DEPART_CITY_TAB_POSITION)
56 | }
57 | }
58 |
59 | }
60 |
61 | describe("user interactions") {
62 |
63 | beforeEachTest {
64 | presenter.attachView(view)
65 | }
66 |
67 | describe("on tab selected") {
68 | beforeEachTest {
69 | presenter.onTabSelected(WeatherForecastPresenter.ARRIVE_CITY_TAB_POSITION)
70 | presenter.onTabSelected(WeatherForecastPresenter.DEPART_CITY_TAB_POSITION)
71 | }
72 |
73 | it("should open tab depart tab") {
74 | verifyOrder {
75 | view.showForecastForCities(departCity, arriveCity)
76 | view.openForecastPage(WeatherForecastPresenter.DEPART_CITY_TAB_POSITION)
77 | view.openForecastPage(WeatherForecastPresenter.ARRIVE_CITY_TAB_POSITION)
78 | view.openForecastPage(WeatherForecastPresenter.DEPART_CITY_TAB_POSITION)
79 | }
80 | }
81 |
82 | }
83 |
84 | }
85 |
86 | })
87 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/presentation/ui/weatherforecast/HourlyForecastAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.ImageView
8 | import android.widget.TextView
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.utair.core.R
11 | import com.utair.domain.global.models.WeatherForecast
12 | import com.utair.domain.global.models.WeatherForecast.WeatherCondition
13 | import com.utair.presentation.ui.weatherforecast.HourlyForecastAdapter.ProductViewHolder
14 | import java.text.SimpleDateFormat
15 |
16 | class HourlyForecastAdapter(
17 | context: Context,
18 | private val hourlyForecasts: List
19 | ) : RecyclerView.Adapter() {
20 |
21 | private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
22 | private val timeFormatter = SimpleDateFormat("HH:mm")
23 |
24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
25 | val view = layoutInflater.inflate(R.layout.item_hourly_forecast, parent, false)
26 | return ProductViewHolder(view)
27 | }
28 |
29 | override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
30 | val hourlyForecast = hourlyForecasts[position]
31 | holder.time.text = timeFormatter.format(hourlyForecast.dateTime.toDate())
32 | holder.weatherIcon.setImageResource(getIconForCondition(hourlyForecast.condition))
33 | holder.temperature.text = String.format("%.2f °C", hourlyForecast.temperature)
34 | holder.windSpeed.text = String.format("%.2f m/s", hourlyForecast.speed)
35 | }
36 |
37 | override fun getItemCount() = hourlyForecasts.size
38 |
39 | private fun getIconForCondition(weatherCondition: WeatherCondition?): Int {
40 | return when (weatherCondition) {
41 | WeatherCondition.SUNNY -> R.drawable.ic_sun
42 | WeatherCondition.FOG -> R.drawable.ic_fog
43 | WeatherCondition.LIGHT_CLOUDS -> R.drawable.ic_light_clouds
44 | WeatherCondition.CLOUDS -> R.drawable.ic_clouds
45 | WeatherCondition.LIGHT_RAIN -> R.drawable.ic_light_rain
46 | WeatherCondition.RAIN -> R.drawable.ic_rain
47 | WeatherCondition.SNOW -> R.drawable.ic_snowflake
48 | WeatherCondition.STORM -> R.drawable.ic_storm
49 | else -> R.drawable.ic_question
50 | }
51 | }
52 |
53 | class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
54 | val time: TextView
55 | val weatherIcon: ImageView
56 | val temperature: TextView
57 | val windSpeed: TextView
58 |
59 | init {
60 | time = itemView.findViewById(R.id.forecast_time) as TextView
61 | weatherIcon = itemView.findViewById(R.id.weather_icon) as ImageView
62 | temperature = itemView.findViewById(R.id.temperature) as TextView
63 | windSpeed = itemView.findViewById(R.id.wind_speed) as TextView
64 | }
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_clouds.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
25 |
29 |
34 |
41 |
46 |
--------------------------------------------------------------------------------
/app-coroutines/src/test/java/com/utair/presentation/mvp/CityWeatherForecastPresenterTest.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.mvp
2 |
3 | import com.utair.di.PrimitiveWrapper
4 | import com.utair.domain.global.exceptions.NoNetworkException
5 | import com.utair.domain.global.models.WeatherForecast
6 | import com.utair.domain.weatherforecast.WeatherForecastInteractor
7 | import com.utair.presentation.mvp.global.ErrorHandler
8 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastPresenter
9 | import com.utair.presentation.mvp.weatherforecast.CityWeatherForecastView
10 | import io.mockk.coEvery
11 | import io.mockk.coVerify
12 | import io.mockk.every
13 | import io.mockk.mockk
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.test.resetMain
16 | import kotlinx.coroutines.test.setMain
17 | import org.spekframework.spek2.Spek
18 | import org.spekframework.spek2.lifecycle.CachingMode
19 | import org.spekframework.spek2.style.specification.describe
20 |
21 | class CityWeatherForecastPresenterTest : Spek({
22 |
23 | val cityName = "Moscow"
24 | val cityNameWrapper by memoized { mockk>() }
25 | val interactor by memoized { mockk() }
26 | val errorHandler by memoized { mockk() }
27 | val view by memoized { mockk(relaxed = true) }
28 |
29 | val presenter by memoized(CachingMode.TEST) {
30 | CityWeatherForecastPresenter(
31 | cityName = cityNameWrapper,
32 | weatherForecastInteractor = interactor,
33 | errorHandler = errorHandler
34 | )
35 | }
36 |
37 | val weatherForecast by memoized(CachingMode.TEST) {
38 | mockk()
39 | }
40 |
41 | beforeEachTest {
42 | every { cityNameWrapper.value } returns cityName
43 |
44 | Dispatchers.setMain(Dispatchers.Unconfined)
45 | coEvery { interactor.getWeatherForecastForCity(cityName) } returns weatherForecast
46 | }
47 |
48 | afterEachTest {
49 | Dispatchers.resetMain()
50 | }
51 |
52 | describe("start actions") {
53 | describe("forecasts loading") {
54 |
55 | describe("on network is available") {
56 | val dailyForecasts by memoized { mockk>() }
57 | beforeEachTest {
58 | every { weatherForecast.dailyForecasts } returns dailyForecasts
59 | presenter.attachView(view)
60 | }
61 |
62 | it("should show forecast") {
63 | coVerify {
64 | interactor.getWeatherForecastForCity(cityName)
65 | view.showForecast(dailyForecasts)
66 | }
67 | }
68 | }
69 |
70 | describe("on no network error") {
71 | beforeEachTest {
72 | coEvery { interactor.getWeatherForecastForCity(cityName) } throws NoNetworkException("Error message")
73 | presenter.attachView(view)
74 | }
75 |
76 | it("should show network error") {
77 | coVerify {
78 | interactor.getWeatherForecastForCity(cityName)
79 | view.showNoNetworkMessage()
80 | }
81 | }
82 | }
83 | }
84 |
85 | }
86 |
87 | })
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Clean Architecture + MVP Sample
2 |
3 |
4 |
5 |
6 | The sample app that demonstrates using Clean Architecture + MVP.
7 |
8 | ### 🏛 Project Structure
9 |
10 | The project contains 3 modules:
11 | - **core** - contains common code for both modules
12 | - **app-coroutines** - Coroutines, Toothpick
13 | - **app-rxjava** - RxJava 2, Dagger 2
14 |
15 | This project includes the following libraries, tools and solutions:
16 |
17 | - [Clean Architecture](https://github.com/ImangazalievM/CleanArchitectureManifest)
18 | - [Dagger 2](https://github.com/google/dagger) / [Toothpick](https://github.com/stephanenicolas/toothpick) - for dependency injection
19 | - [RxJava 2](https://github.com/ReactiveX/RxJava) / [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - for multithreading
20 | - [Moxy](https://github.com/Arello-Mobile/Moxy) - for MVP pattern implementation
21 | - [Alligator](https://github.com/aartikov/Alligator) - for screens navigatio
22 | - [Spek](https://github.com/spekframework/spek) + [MockK](https://github.com/mockk/mockk) + [Strikt](https://github.com/robfletcher/strikt/) - for unit-tests
23 | - [Kaspresso](https://github.com/KasperskyLab/Kaspresso) - for UI-tests
24 | - Gradle Kotlin DSL
25 |
26 | ### 🌦 Open Weather API
27 |
28 | The app uses [OpenWeather API](https://openweathermap.org/api) for receiving weather forecasts,
29 | so to build the project you have to provide API key. To do it create account on OpenWeather website,
30 | then generate your own API key and put it to `open_weather_api_key` property in `api_keys.properties` file.
31 |
32 | ### ⚠ Attention:
33 | Clean Architecture approach [recommends](https://github.com/ImangazalievM/CleanArchitectureManifest#repository) us to create interfaces for repositories, so domain layer shouldn't know anything about data layer. The main goal of this rule is ability to test our interactors using simple unit-tests. IMHO, it is redundant because using repositories implementation directly doesn't cause any problems.
34 |
35 | ### 🚦️ Tests Runnning
36 |
37 | UI and unit-tests are contained in the **app-coroutines** module.
38 |
39 | To run unit-tests, you need to setup [Spek Framework](https://plugins.jetbrains.com/plugin/10915-spek-framework) plugin.
40 |
41 | ## 🤝 License
42 | ```
43 | The MIT License
44 |
45 | Copyright (c) 2020 Mahach Imangazaliev
46 |
47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
48 |
49 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
50 |
51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
52 | ```
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/ui/weatherforecast/WeatherForecastActivity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import android.os.Bundle
4 | import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener
5 | import com.arellomobile.mvp.presenter.InjectPresenter
6 | import com.arellomobile.mvp.presenter.ProvidePresenter
7 | import com.utair.core.databinding.ActivityWeatherForecastBinding
8 | import com.utair.di.bindPrimitive
9 | import com.utair.di.get
10 | import com.utair.di.installModule
11 | import com.utair.di.qualifiers.ArriveCity
12 | import com.utair.di.qualifiers.DepartCity
13 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter
14 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter.Companion.ARRIVE_CITY_TAB_POSITION
15 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter.Companion.DEPART_CITY_TAB_POSITION
16 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastView
17 | import com.utair.presentation.ui.global.base.BaseActivity
18 | import com.utair.presentation.ui.global.navigation.WeatherForecastScreen
19 | import toothpick.Scope
20 |
21 | class WeatherForecastActivity : BaseActivity(), WeatherForecastView {
22 |
23 | override val scopeModuleInstaller: (Scope) -> Unit = {
24 | val screen = screenResolver.getScreen(this)
25 | it.installModule {
26 | bindPrimitive(DepartCity::class, screen.departCity)
27 | bindPrimitive(ArriveCity::class, screen.arriveCity)
28 | }
29 | }
30 |
31 | private val binding by lazy {
32 | ActivityWeatherForecastBinding.inflate(layoutInflater)
33 | }
34 |
35 | @InjectPresenter
36 | lateinit var presenter: WeatherForecastPresenter
37 |
38 | @ProvidePresenter
39 | fun providePresenter() = scope.get()
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | setContentView(binding.root)
44 |
45 | binding.toolbar.toolbar.setNavigationOnClickListener { onBackPressed() }
46 | binding.tabs.forwardTab.setOnClickListener {
47 | presenter.onTabSelected(DEPART_CITY_TAB_POSITION)
48 | }
49 | binding.tabs.returnTab.setOnClickListener {
50 | presenter.onTabSelected(ARRIVE_CITY_TAB_POSITION)
51 | }
52 | binding.citiesForecastPager.addOnPageChangeListener(object : SimpleOnPageChangeListener() {
53 | override fun onPageSelected(position: Int) {
54 | presenter.onTabSelected(position)
55 | }
56 | })
57 | }
58 |
59 | override fun showForecastForCities(departCity: String, arriveCity: String) {
60 | binding.toolbar.departCityLabel.text = departCity
61 | binding.toolbar.arriveCityLabel.text = arriveCity
62 |
63 | binding.citiesForecastPager.adapter = CitiesForecastPagerAdapter(
64 | fragmentManager = supportFragmentManager,
65 | departCityName = departCity,
66 | arriveCityName = arriveCity,
67 | fragmentConstructor = {
68 | CityWeatherForecastFragment.newInstance(it)
69 | }
70 | )
71 | }
72 |
73 | override fun openForecastPage(position: Int) {
74 | binding.tabs.forwardTab.isSelected = position == DEPART_CITY_TAB_POSITION
75 | binding.tabs.returnTab.isSelected = position == ARRIVE_CITY_TAB_POSITION
76 | binding.citiesForecastPager.currentItem = position
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/app-rxjava/src/main/java/com/utair/presentation/ui/weatherforecast/WeatherForecastActivity.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.weatherforecast
2 |
3 | import android.os.Bundle
4 | import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener
5 | import com.arellomobile.mvp.presenter.InjectPresenter
6 | import com.arellomobile.mvp.presenter.ProvidePresenter
7 | import com.utair.core.databinding.ActivityWeatherForecastBinding
8 | import com.utair.di.presenters.weatherforecast.DaggerWeatherForecastComponent
9 | import com.utair.di.presenters.weatherforecast.WeatherForecastPresenterModule
10 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter
11 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter.Companion.ARRIVE_CITY_TAB_POSITION
12 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastPresenter.Companion.DEPART_CITY_TAB_POSITION
13 | import com.utair.presentation.mvp.weatherforecast.WeatherForecastView
14 | import com.utair.presentation.ui.global.base.BaseMvpActivity
15 | import com.utair.presentation.ui.global.navigation.WeatherForecastScreen
16 |
17 | class WeatherForecastActivity : BaseMvpActivity(), WeatherForecastView {
18 |
19 | private val binding by lazy {
20 | ActivityWeatherForecastBinding.inflate(layoutInflater)
21 | }
22 |
23 | @InjectPresenter
24 | lateinit var presenter: WeatherForecastPresenter
25 |
26 | @ProvidePresenter
27 | fun providePresenter(): WeatherForecastPresenter {
28 | val screen = screenResolver.getScreen(this)
29 | return DaggerWeatherForecastComponent.builder()
30 | .applicationComponent(appComponent)
31 | .weatherForecastPresenterModule(WeatherForecastPresenterModule(
32 | departCity = screen.departCity,
33 | arriveCity = screen.arriveCity
34 | ))
35 | .build()
36 | .getWeatherForecastPresenter()
37 | }
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 | setContentView(binding.root)
42 |
43 | binding.toolbar.toolbar.setNavigationOnClickListener { onBackPressed() }
44 | binding.tabs.forwardTab.setOnClickListener {
45 | presenter.onTabSelected(DEPART_CITY_TAB_POSITION)
46 | }
47 | binding.tabs.returnTab.setOnClickListener {
48 | presenter.onTabSelected(ARRIVE_CITY_TAB_POSITION)
49 | }
50 | binding.citiesForecastPager.addOnPageChangeListener(object : SimpleOnPageChangeListener() {
51 | override fun onPageSelected(position: Int) {
52 | presenter.onTabSelected(position)
53 | }
54 | })
55 | }
56 |
57 | override fun showForecastForCities(departCity: String, arriveCity: String) {
58 | binding.toolbar.departCityLabel.text = departCity
59 | binding.toolbar.arriveCityLabel.text = arriveCity
60 |
61 | binding.citiesForecastPager.adapter = CitiesForecastPagerAdapter(
62 | fragmentManager = supportFragmentManager,
63 | departCityName = departCity,
64 | arriveCityName = arriveCity,
65 | fragmentConstructor = {
66 | CityWeatherForecastFragment.newInstance(it)
67 | }
68 | )
69 | }
70 |
71 | override fun openForecastPage(position: Int) {
72 | binding.tabs.forwardTab.isSelected = position == DEPART_CITY_TAB_POSITION
73 | binding.tabs.returnTab.isSelected = position == ARRIVE_CITY_TAB_POSITION
74 | binding.citiesForecastPager.currentItem = position
75 | }
76 |
77 |
78 | }
--------------------------------------------------------------------------------
/core/src/main/res/drawable/ic_light_clouds.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
16 |
22 |
28 |
31 |
35 |
40 |
44 |
48 |
53 |
58 |
63 |
--------------------------------------------------------------------------------
/core/src/main/java/com/utair/data/global/network/mappers/WeatherForecastResponseMapper.kt:
--------------------------------------------------------------------------------
1 | package com.utair.data.global.network.mappers
2 |
3 | import com.utair.data.global.network.responses.WeatherForecastResponse
4 | import com.utair.domain.global.models.WeatherForecast
5 | import com.utair.domain.global.models.WeatherForecast.DailyForecast
6 | import com.utair.domain.global.models.WeatherForecast.WeatherCondition
7 | import org.joda.time.DateTime
8 | import java.util.*
9 | import javax.inject.Inject
10 |
11 | class WeatherForecastResponseMapper @Inject constructor() {
12 |
13 | fun map(response: WeatherForecastResponse): WeatherForecast {
14 | val hourlyForecastEntities: MutableList = ArrayList()
15 | val hourlyForecastResponses = response.hourlyForecasts
16 | for (hourlyForecastResponse in hourlyForecastResponses) {
17 | // convert from unix timestamp to date, multiply
18 | // by thousand to convert seconds to milliseconds
19 | val dateTime = DateTime(hourlyForecastResponse.timestamp * 1000)
20 | val condition = getConditionById(hourlyForecastResponse.weatherId)
21 | val temperature = hourlyForecastResponse.temperature
22 | val speed = hourlyForecastResponse.speed
23 | hourlyForecastEntities.add(WeatherForecast.HourlyForecast(dateTime, condition, temperature, speed))
24 | }
25 | val dailyForecastEntities = splitForecastsByDays(hourlyForecastEntities)
26 | return WeatherForecast(response.cityName, dailyForecastEntities)
27 | }
28 |
29 | /**
30 | * Returns weather conditions by identifier
31 | * More information about weather IDs can be
32 | * found here https://openweathermap.org/weather-conditions
33 | */
34 | private fun getConditionById(weatherId: Int): WeatherCondition {
35 | if (weatherId in 200..232) {
36 | return WeatherCondition.STORM
37 | } else if (weatherId in 300..321) {
38 | return WeatherCondition.LIGHT_RAIN
39 | } else if (weatherId in 500..504) {
40 | return WeatherCondition.RAIN
41 | } else if (weatherId == 511) {
42 | return WeatherCondition.SNOW
43 | } else if (weatherId in 520..531) {
44 | return WeatherCondition.RAIN
45 | } else if (weatherId in 600..622) {
46 | return WeatherCondition.SNOW
47 | } else if (weatherId in 701..761) {
48 | return WeatherCondition.FOG
49 | } else if (weatherId == 761 || weatherId == 781) {
50 | return WeatherCondition.STORM
51 | } else if (weatherId == 800) {
52 | return WeatherCondition.SUNNY
53 | } else if (weatherId == 801) {
54 | return WeatherCondition.LIGHT_CLOUDS
55 | } else if (weatherId in 802..804) {
56 | return WeatherCondition.CLOUDS
57 | }
58 | return WeatherCondition.UNKNOWN
59 | }
60 |
61 | /**
62 | * Split hourly forecast by day
63 | */
64 | private fun splitForecastsByDays(hourlyForecastEntities: List): List {
65 | //sort the hourly forecast by ascending time
66 | Collections.sort(hourlyForecastEntities) { forecast1: WeatherForecast.HourlyForecast, forecast2: WeatherForecast.HourlyForecast -> forecast1.dateTime.compareTo(forecast2.dateTime) }
67 | val forecastsByDay: MutableMap = HashMap()
68 | for (hourlyForecast in hourlyForecastEntities) {
69 | val forecastDate = hourlyForecast.dateTime
70 | val dateStringKey = forecastDate.dayOfMonth.toString() + "." + forecastDate.monthOfYear + "." + forecastDate.year
71 | var dailyForecast = forecastsByDay[dateStringKey]
72 | if (dailyForecast == null) {
73 | dailyForecast = DailyForecast(DateTime(forecastDate))
74 | forecastsByDay[dateStringKey] = dailyForecast
75 | }
76 | dailyForecast.addHourForecast(hourlyForecast)
77 | }
78 | val dailyForecastEntities: List = ArrayList(forecastsByDay.values)
79 |
80 | //sort the forecast by date
81 | Collections.sort(dailyForecastEntities) { forecast1: DailyForecast, forecast2: DailyForecast -> forecast1.date.compareTo(forecast2.date) }
82 | return dailyForecastEntities
83 | }
84 | }
--------------------------------------------------------------------------------
/core/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
12 |
13 |
17 |
18 |
23 |
24 |
28 |
29 |
34 |
35 |
40 |
41 |
46 |
47 |
52 |
53 |
60 |
61 |
67 |
68 |
73 |
74 |
79 |
80 |
87 |
88 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/app-coroutines/src/main/java/com/utair/presentation/ui/global/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.utair.presentation.ui.global.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.Toast
8 | import com.utair.DI
9 | import com.utair.UTairApplication
10 | import com.utair.di.get
11 | import com.utair.di.objectScopeName
12 | import me.aartikov.alligator.AndroidNavigator
13 | import me.aartikov.alligator.NavigationContextBinder
14 | import me.aartikov.alligator.ScreenResolver
15 | import timber.log.Timber
16 | import toothpick.Scope
17 | import toothpick.Toothpick
18 | /**
19 | * @author Konstantin Tskhovrebov (aka terrakok) on 26.03.17.
20 | */
21 | abstract class BaseFragment : MvpAppCompatFragment() {
22 |
23 | companion object {
24 | private const val STATE_LAUNCH_FLAG = "state_launch_flag"
25 | private const val STATE_SCOPE_NAME = "state_scope_name"
26 | private const val STATE_SCOPE_WAS_CLOSED = "state_scope_was_closed"
27 | }
28 |
29 | private var instanceStateSaved: Boolean = false
30 |
31 | protected open val parentScopeName: String by lazy {
32 | (parentFragment as? BaseFragment)?.scopeName
33 | ?: (activity as BaseActivity).scopeName
34 | }
35 |
36 | protected lateinit var scope: Scope
37 | private lateinit var scopeName: String
38 | protected open val scopeModuleInstaller: (Scope) -> Unit = {}
39 |
40 | private val appScope = Toothpick.openScope(DI.APP_SCOPE)
41 | protected val navigator by lazy { appScope.get() }
42 | protected val navigationContextBinder by lazy { appScope.get() }
43 | protected val screenResolver by lazy { appScope.get() }
44 |
45 | override fun onCreate(savedInstanceState: Bundle?) {
46 | val savedAppCode = savedInstanceState?.getString(STATE_LAUNCH_FLAG)
47 | //False - if fragment was restored without new app process (for example: activity rotation)
48 | val isNewInAppProcess = savedAppCode != getAppLaunchCode()
49 | val scopeWasClosed = savedInstanceState?.getBoolean(STATE_SCOPE_WAS_CLOSED) ?: true
50 | val scopeIsNotInit = isNewInAppProcess || scopeWasClosed
51 | scopeName = savedInstanceState?.getString(STATE_SCOPE_NAME) ?: objectScopeName()
52 | scope = Toothpick.openScopes(parentScopeName, scopeName)
53 | .apply {
54 | if (scopeIsNotInit) {
55 | Timber.d("Init new UI scope: $scopeName")
56 | scopeModuleInstaller(this)
57 | } else {
58 | Timber.d("Get exist UI scope: $scopeName")
59 | }
60 | }
61 | super.onCreate(savedInstanceState)
62 | }
63 |
64 | override fun onResume() {
65 | super.onResume()
66 | instanceStateSaved = false
67 | }
68 |
69 | override fun onDestroy() {
70 | super.onDestroy()
71 |
72 | if (needCloseScope()) {
73 | //destroy this fragment with scope
74 | Timber.d("Destroy UI scope: $scopeName")
75 | Toothpick.closeScope(scope.name)
76 | }
77 | }
78 |
79 | override fun onSaveInstanceState(outState: Bundle) {
80 | super.onSaveInstanceState(outState)
81 |
82 | instanceStateSaved = true
83 | outState.putString(STATE_SCOPE_NAME, scopeName)
84 | outState.putString(STATE_LAUNCH_FLAG, getAppLaunchCode())
85 | outState.putBoolean(STATE_SCOPE_WAS_CLOSED, needCloseScope()) //save it but will be used only if destroyed
86 | }
87 |
88 | fun showMessage(message: String) {
89 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
90 | }
91 |
92 | private fun getAppLaunchCode(): String = (context!!.applicationContext as UTairApplication).appLaunchCode
93 |
94 | //This is android, baby!
95 | private fun isRealRemoving(): Boolean =
96 | (isRemoving && !instanceStateSaved) //because isRemoving == true for fragment in backstack on screen rotation
97 | || ((parentFragment as? BaseFragment)?.isRealRemoving() ?: false)
98 |
99 | //It will be valid only for 'onDestroy()' method
100 | private fun needCloseScope(): Boolean =
101 | when {
102 | activity?.isChangingConfigurations == true -> false
103 | activity?.isFinishing == true -> true
104 | else -> isRealRemoving()
105 | }
106 |
107 | }
--------------------------------------------------------------------------------