├── 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 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------