Repository: i-vishi/shopping-android-app
Branch: master
Commit: 1768ebf52bb9
Files: 223
Total size: 593.6 KB
Directory structure:
gitextract_6suav1uc/
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── .name
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── compiler.xml
│ ├── gradle.xml
│ ├── jarRepositories.xml
│ ├── misc.xml
│ ├── render.experimental.xml
│ ├── runConfigurations.xml
│ └── vcs.xml
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── vishalgaur/
│ │ └── shoppingapp/
│ │ ├── AppDatabaseTest.kt
│ │ ├── ClickClickableSpan.kt
│ │ ├── ExampleInstrumentedTest.kt
│ │ ├── LiveDataTestUtil.kt
│ │ ├── MainCoroutineRule.kt
│ │ ├── RecyclerViewMatcherUtils.kt
│ │ ├── data/
│ │ │ └── source/
│ │ │ ├── FakeAuthRepository.kt
│ │ │ ├── FakeProductsDataSource.kt
│ │ │ ├── FakeProductsRepository.kt
│ │ │ ├── FakeUserDataSource.kt
│ │ │ └── repository/
│ │ │ ├── AuthRepositoryTest.kt
│ │ │ └── ProductsRepositoryTest.kt
│ │ ├── ui/
│ │ │ ├── home/
│ │ │ │ ├── HomeFragmentTest.kt
│ │ │ │ └── ProductDetailsFragmentTest.kt
│ │ │ └── loginSignup/
│ │ │ ├── LoginFragmentTest.kt
│ │ │ └── SignupFragmentTest.kt
│ │ └── viewModels/
│ │ ├── AddEditAddressViewModelTest.kt
│ │ ├── AddEditProductViewModelTest.kt
│ │ ├── AuthViewModelTest.kt
│ │ ├── HomeViewModelTest.kt
│ │ ├── OrderViewModelTest.kt
│ │ └── ProductViewModelTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── vishalgaur/
│ │ │ └── shoppingapp/
│ │ │ ├── ServiceLocator.kt
│ │ │ ├── ShoppingApplication.kt
│ │ │ ├── Utils.kt
│ │ │ ├── data/
│ │ │ │ ├── Product.kt
│ │ │ │ ├── Result.kt
│ │ │ │ ├── ShoppingAppSessionManager.kt
│ │ │ │ ├── UserData.kt
│ │ │ │ ├── source/
│ │ │ │ │ ├── ProductDataSource.kt
│ │ │ │ │ ├── UserDataSource.kt
│ │ │ │ │ ├── local/
│ │ │ │ │ │ ├── ProductsDao.kt
│ │ │ │ │ │ ├── ProductsLocalDataSource.kt
│ │ │ │ │ │ ├── ShoppingAppDatabase.kt
│ │ │ │ │ │ ├── UserDao.kt
│ │ │ │ │ │ └── UserLocalDataSource.kt
│ │ │ │ │ ├── remote/
│ │ │ │ │ │ ├── AuthRemoteDataSource.kt
│ │ │ │ │ │ └── ProductsRemoteDataSource.kt
│ │ │ │ │ └── repository/
│ │ │ │ │ ├── AuthRepoInterface.kt
│ │ │ │ │ ├── AuthRepository.kt
│ │ │ │ │ ├── ProductsRepoInterface.kt
│ │ │ │ │ └── ProductsRepository.kt
│ │ │ │ └── utils/
│ │ │ │ ├── DateTypeConvertors.kt
│ │ │ │ ├── EmailMobileData.kt
│ │ │ │ ├── ListTypeConverter.kt
│ │ │ │ ├── ObjectListTypeConvertor.kt
│ │ │ │ ├── ProductUtils.kt
│ │ │ │ └── Utils.kt
│ │ │ ├── ui/
│ │ │ │ ├── LaunchActivity.kt
│ │ │ │ ├── RecyclerViewPaddingItemDecoration.kt
│ │ │ │ ├── UiUtils.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── AccountFragment.kt
│ │ │ │ │ ├── AddEditAddressFragment.kt
│ │ │ │ │ ├── AddEditProductFragment.kt
│ │ │ │ │ ├── AddProductImagesAdapter.kt
│ │ │ │ │ ├── AddressAdapter.kt
│ │ │ │ │ ├── AddressFragment.kt
│ │ │ │ │ ├── CartFragment.kt
│ │ │ │ │ ├── CartItemAdapter.kt
│ │ │ │ │ ├── FavoritesFragment.kt
│ │ │ │ │ ├── HomeFragment.kt
│ │ │ │ │ ├── LikedProductAdapter.kt
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ ├── OrderDetailsFragment.kt
│ │ │ │ │ ├── OrderProductsAdapter.kt
│ │ │ │ │ ├── OrderSuccessFragment.kt
│ │ │ │ │ ├── OrdersAdapter.kt
│ │ │ │ │ ├── OrdersFragment.kt
│ │ │ │ │ ├── PayByAdapter.kt
│ │ │ │ │ ├── ProductAdapter.kt
│ │ │ │ │ ├── ProductDetailsFragment.kt
│ │ │ │ │ ├── ProductImagesAdapter.kt
│ │ │ │ │ ├── ProfileFragment.kt
│ │ │ │ │ ├── SelectAddressFragment.kt
│ │ │ │ │ └── SelectPaymentFragment.kt
│ │ │ │ └── loginSignup/
│ │ │ │ ├── LoginFragment.kt
│ │ │ │ ├── LoginSignupActivity.kt
│ │ │ │ ├── LoginSignupBaseFragment.kt
│ │ │ │ ├── OtpActivity.kt
│ │ │ │ └── SignupFragment.kt
│ │ │ └── viewModels/
│ │ │ ├── AddEditAddressViewModel.kt
│ │ │ ├── AddEditProductViewModel.kt
│ │ │ ├── AuthViewModel.kt
│ │ │ ├── HomeViewModel.kt
│ │ │ ├── OrderViewModel.kt
│ │ │ ├── OtpViewModel.kt
│ │ │ └── ProductViewModel.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── address_account_drawable.xml
│ │ │ ├── avatar_background.xml
│ │ │ ├── bottom_nav_selector.xml
│ │ │ ├── btn_gradient.xml
│ │ │ ├── card_item_selector.xml
│ │ │ ├── color_radio_normal.xml
│ │ │ ├── color_radio_selected.xml
│ │ │ ├── color_radio_selector.xml
│ │ │ ├── dotted_line_drawable.xml
│ │ │ ├── heart_icon_drawable.xml
│ │ │ ├── ic_add_24.xml
│ │ │ ├── ic_add_48.xml
│ │ │ ├── ic_add_shopping_cart_24.xml
│ │ │ ├── ic_baseline_person_24.xml
│ │ │ ├── ic_baseline_shopping_cart_24.xml
│ │ │ ├── ic_cancel_24.xml
│ │ │ ├── ic_chevron_left_48.xml
│ │ │ ├── ic_delete_24.xml
│ │ │ ├── ic_edit_24.xml
│ │ │ ├── ic_favorite_filled_24.xml
│ │ │ ├── ic_favorite_outlined_24.xml
│ │ │ ├── ic_filled_check_circle_24.xml
│ │ │ ├── ic_filled_library_books_24.xml
│ │ │ ├── ic_filled_location_on_24.xml
│ │ │ ├── ic_filled_logout_24.xml
│ │ │ ├── ic_filled_person_24.xml
│ │ │ ├── ic_filter_alt_24.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_menu_24.xml
│ │ │ ├── ic_outline_arrow_back_24.xml
│ │ │ ├── ic_outline_email_24.xml
│ │ │ ├── ic_outline_library_books_24.xml
│ │ │ ├── ic_outline_phone_android_24.xml
│ │ │ ├── ic_outlined_home_24.xml
│ │ │ ├── ic_outlined_person_24.xml
│ │ │ ├── ic_outlined_shopping_cart_24.xml
│ │ │ ├── ic_remove_24.xml
│ │ │ ├── ic_remove_shopping_cart_24.xml
│ │ │ ├── ic_search_24.xml
│ │ │ ├── layout_background_rounded_corners.xml
│ │ │ ├── liked_heart_drawable.xml
│ │ │ ├── login_bg_img.xml
│ │ │ ├── orders_account_drawable.xml
│ │ │ ├── person_account_drawable.xml
│ │ │ ├── radio_normal.xml
│ │ │ ├── radio_selected.xml
│ │ │ ├── radio_selector.xml
│ │ │ ├── round_button.xml
│ │ │ ├── round_outline_rect.xml
│ │ │ ├── signout_account_drawable.xml
│ │ │ └── sl_favourite_24dp.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── font/
│ │ │ ├── nunito_sans.xml
│ │ │ └── nunito_sans_extrabold.xml
│ │ ├── layout/
│ │ │ ├── activity_launch.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_otp.xml
│ │ │ ├── activity_signup.xml
│ │ │ ├── add_images_item.xml
│ │ │ ├── cart_list_item.xml
│ │ │ ├── country_list_item.xml
│ │ │ ├── fragment_account.xml
│ │ │ ├── fragment_add_edit_address.xml
│ │ │ ├── fragment_add_edit_product.xml
│ │ │ ├── fragment_address.xml
│ │ │ ├── fragment_cart.xml
│ │ │ ├── fragment_favorites.xml
│ │ │ ├── fragment_home.xml
│ │ │ ├── fragment_login.xml
│ │ │ ├── fragment_order_details.xml
│ │ │ ├── fragment_order_success.xml
│ │ │ ├── fragment_orders.xml
│ │ │ ├── fragment_product_details.xml
│ │ │ ├── fragment_profile.xml
│ │ │ ├── fragment_select_address.xml
│ │ │ ├── fragment_select_payment.xml
│ │ │ ├── fragment_signup.xml
│ │ │ ├── images_item.xml
│ │ │ ├── layout_address_card.xml
│ │ │ ├── layout_circular_loader.xml
│ │ │ ├── layout_home_ad.xml
│ │ │ ├── layout_home_top_app_bar.xml
│ │ │ ├── layout_list_item.xml
│ │ │ ├── layout_loader_card.xml
│ │ │ ├── layout_no_icon_app_bar.xml
│ │ │ ├── layout_order_summary_card.xml
│ │ │ ├── layout_price_card.xml
│ │ │ ├── layout_shipping_card.xml
│ │ │ ├── layout_top_bar.xml
│ │ │ └── products_list_item.xml
│ │ ├── menu/
│ │ │ ├── app_bar_menu.xml
│ │ │ ├── bottom_navigation_menu.xml
│ │ │ ├── home_app_bar_menu.xml
│ │ │ ├── menu_main.xml
│ │ │ └── menu_with_add_only.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── navigation/
│ │ │ ├── home_nav_graph.xml
│ │ │ └── signup_nav_graph.xml
│ │ ├── values/
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── font_certs.xml
│ │ │ ├── preloaded_fonts.xml
│ │ │ ├── shapes.xml
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml
│ │ │ ├── themes.xml
│ │ │ └── type.xml
│ │ └── values-night/
│ │ └── themes.xml
│ └── test/
│ └── java/
│ └── com/
│ └── vishalgaur/
│ └── shoppingapp/
│ └── UtilsTest.kt
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: .idea/.gitignore
================================================
================================================
FILE: .idea/.name
================================================
Shopping App
================================================
FILE: .idea/codeStyles/Project.xml
================================================
.*:id
http://schemas.android.com/apk/res/android
.*:name
http://schemas.android.com/apk/res/android
.*
http://schemas.android.com/apk/res/android
ANDROID_ATTRIBUTE_ORDER
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
================================================
FILE: .idea/compiler.xml
================================================
================================================
FILE: .idea/gradle.xml
================================================
================================================
FILE: .idea/jarRepositories.xml
================================================
================================================
FILE: .idea/misc.xml
================================================
================================================
FILE: .idea/render.experimental.xml
================================================
================================================
FILE: .idea/runConfigurations.xml
================================================
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Vishal Gaur
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
================================================
FILE: README.md
================================================
# Shopping Android App
An e-commerce android application written in Kotlin where users can sell and buy products.
## Overview
The application contains list of products such as shoes, slippers on which user can click to view its details and then, add them to cart. User can like and dislike the product as well. Also, User can sell products, if he/she signed up as a Seller.
Some other features are as following:
- Login / Signup with OTP Verification.
- Recyclerview with variable span size to show products.
- Search Bar and filtering
- Product detail screen with image carousel and custom Radio Buttons.
- Add/Edit Product for Sellers
- See all orders placed.
- Increase/Decrease quantity of product in cart.
- Place Order.
- Modify status of order for Seller.
- Add/Edit Address
- Tested using Espresso. Written unit, instrumentation and UI tests.
## Some Screenshots
| Splash Screen | Application Home | Product Detail |
| :----------------------------------: | :---------------------------------------: | :----------------------------------:|
|  |  |  |
| Signup | Login | OTP Verification |
| :---------------------------------: | :-------------------------------: | :------------------------------:|
|  |  |  |
| Shopping Cart | Address Selection | Payment Method | Order Success |
| :------------------------------: | :----------------------------------------: | :-------------------------------------:| :---------------------------------------: |
|  |  |  |  |
| Add Product | All Orders | Order Detail | Sign Out |
| :-------------------------------------: | :--------------------------------: | :---------------------------------------:| :----------------------------------: |
|  |  |  |  |
## Project Setup
### Clone and install
Clone this repository and import into Android Studio
```
git clone https://github.com/i-vishi/shopping-android-app.git
```
### Configuration
- The project requires Firebase. So follow the steps given [here (Add Firebase to Android Project)](https://firebase.google.com/docs/android/setup) to add firebase to your android project.
- Download the firebase config file `google-services.json`
- Move the config file to `(app)` module of the project.
- Also, add Cloud Firestore for storing users, products, orders and addresses data. Follow instructions [here (Add Cloud Firestore to your app)](https://firebase.google.com/docs/firestore/quickstart) to add Cloud Firestore to your app.
- There need to be two collections - `users` for Users data and `products` for Products Data.
- This project also, requires OTP based authentication. So, you just need to enable Phone Number sign-in in your firebase project. Follow instructions [here (Enable Phone Number sign-in)](https://firebase.google.com/docs/auth/android/phone-auth) to enable Phone Number sign-in.
- Do not forget to enable app verification for your firebase project. Follow instructions [here (Enable app verification)](https://firebase.google.com/docs/auth/android/phone-auth#enable-app-verification) to enable app verification. Add both SHA-1 and SHA-256 fingerprints.
Tried everything but still not able to explore the app due to OTP errors? Don't worry, you can by-pass the OTP screen and explore the app.
- Go to `app/src/main/java/com/vishalgaur/shoppingapp/Utils.kt` file.
- Change the return value for function `shouldBypassOTPValidation()` to `true`.
- You are good to go now. Just run the app and explore.
- And take your time to setup the OTP verification. :wink:
## Star History
## Built With
- Kotlin
- Firebase
- Room
- Material
- Glide
---
Made with :blue_heart: by Vishal Gaur
================================================
FILE: app/.gitignore
================================================
/build
/google-services.json
================================================
FILE: app/build.gradle
================================================
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.google.gms.google-services'
id 'kotlin-parcelize'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.vishalgaur.shoppingapp"
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Source: https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx-coroutines-play-services
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
// RecyclerView - beta with ConcatAdapter (previously MergeAdapter)
implementation "androidx.recyclerview:recyclerview:1.2.0"
// Room
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
kapt "androidx.room:room-compiler:$room_version"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-auth-ktx'
// Glide
implementation "com.github.bumptech.glide:glide:$glide_version"
//Firebase
implementation platform('com.google.firebase:firebase-bom:27.0.0')
implementation 'com.google.firebase:firebase-firestore-ktx'
implementation 'com.google.firebase:firebase-storage-ktx'
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation "androidx.test.ext:junit-ktx:1.1.2"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// AndroidJUnitRunner and JUnit Rules
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
// espresso for intents
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
//espresso for recyclerview
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:1.1.2"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "androidx.test:core-ktx:1.3.0"
testImplementation "org.robolectric:robolectric:4.3.1"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
// testing coroutines
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3'
//fragment testing
debugImplementation "androidx.fragment:fragment-testing:1.3.3"
//navigation testing
androidTestImplementation "androidx.navigation:navigation-testing:2.3.5"
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/AppDatabaseTest.kt
================================================
package com.vishalgaur.shoppingapp
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.local.ProductsDao
import com.vishalgaur.shoppingapp.data.source.local.ShoppingAppDatabase
import com.vishalgaur.shoppingapp.data.source.local.UserDao
import kotlinx.coroutines.runBlocking
import org.hamcrest.Matchers.*
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AppDatabaseTest {
private val pro1 = Product(
"pro-owner1-shoe-101",
"Shoe Name 101",
"owner1",
"some description",
"Shoes",
250.0,
300.0,
listOf(5, 6, 7, 8),
listOf("Red", "Blue"),
listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"),
2.5
)
private val pro2 = Product(
"pro-owner1-slipper-101",
"Slipper Name 101",
"owner1",
"some description",
"Slippers",
50.0,
80.0,
listOf(6, 7, 8),
listOf("Black", "Blue"),
listOf(
"http://image-ref-uri/-slipper-101-01.jpg",
"http://image-ref-uri/-slipper-101-02.jpg"
),
4.0
)
private lateinit var userDao: UserDao
private lateinit var productsDao: ProductsDao
private lateinit var appDb: ShoppingAppDatabase
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
appDb =
Room.inMemoryDatabaseBuilder(context, ShoppingAppDatabase::class.java)
.allowMainThreadQueries()
.build()
userDao = appDb.userDao()
productsDao = appDb.productsDao()
}
@After
fun closeDb() {
appDb.clearAllTables()
appDb.close()
}
@Test
fun insertAndGetUser() {
val user = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
ArrayList(),
ArrayList(),
ArrayList()
)
runBlocking {
userDao.insert(user)
val result = userDao.getById("sdjm43892yfh948ehod")
assertThat(result?.userId, `is`(user.userId))
}
}
@Test
fun noData_returnsNull() {
runBlocking {
val result = userDao.getById("1232")
assertThat(result, `is`(nullValue()))
}
}
@Test
fun insertClearUser_returnsNull() {
val user = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
emptyList(),
emptyList(),
emptyList()
)
runBlocking {
userDao.insert(user)
userDao.clear()
val result = userDao.getById("sdjm43892yfh948ehod")
assertThat(result, `is`(nullValue()))
}
}
@Test
fun insertAndGetProduct() {
runBlocking {
productsDao.insert(pro1)
val result = productsDao.getProductById(pro1.productId)
assertEquals(pro1, result)
}
}
@Test
fun insertClearProduct_returnsNull() = runBlocking {
productsDao.insert(pro1)
productsDao.deleteAllProducts()
val result = productsDao.getAllProducts()
assertEquals(0, result.size)
}
@Test
fun deleteProductById() = runBlocking {
productsDao.insert(pro2)
productsDao.deleteProductById(pro2.productId)
val result = productsDao.getProductById(pro2.productId)
assertThat(result, `is`(nullValue()))
}
@Test
fun noProducts_returnsEmptyList() = runBlocking {
val result = productsDao.getAllProducts()
assertThat(result.size, `is`(0))
}
@Test
fun deleteAllProducts_returnsEmptyList() = runBlocking {
productsDao.insert(pro2)
productsDao.deleteAllProducts()
val result = productsDao.getAllProducts()
assertThat(result.size, `is`(0))
}
@Test
fun getProductsByOwner_returnsData() = runBlocking {
productsDao.insert(pro2)
val result = productsDao.getProductsByOwnerId(pro2.owner)
assertThat(result.size, `is`(1))
}
@Test
fun insertMultipleProducts() = runBlocking {
productsDao.insertListOfProducts(listOf(pro1, pro2))
val result = productsDao.getAllProducts()
assertThat(result.size, `is`(2))
}
@Test
fun observeProducts_returnsLiveData() = runBlocking {
val initialRes = productsDao.observeProducts()
productsDao.insert(pro1)
val newValue = productsDao.observeProducts().getOrAwaitValue()
assertThat(initialRes.value, not(newValue))
assertThat(initialRes.value, `is`(nullValue()))
assertThat(newValue.size, `is`(1))
}
@Test
fun observeProductsByOwner_returnsLiveData() = runBlocking {
val initialRes = productsDao.observeProductsByOwner(pro1.owner)
productsDao.insert(pro1)
val newValue = productsDao.observeProductsByOwner(pro1.owner).getOrAwaitValue()
assertThat(initialRes.value, not(newValue))
assertThat(initialRes.value, `is`(nullValue()))
assertThat(newValue.size, `is`(1))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ClickClickableSpan.kt
================================================
package com.vishalgaur.shoppingapp
import android.text.SpannableString
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import org.hamcrest.Matcher
import org.hamcrest.Matchers
internal fun clickClickableSpan(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan"
}
override fun perform(uiController: UiController, view: View) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
// Get the links inside the TextView and check if we find textToClick
val spans =
spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString() == sequence.toString()) {
span.onClick(textView)
return
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ExampleInstrumentedTest.kt
================================================
package com.vishalgaur.shoppingapp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.vishalgaur.shoppingapp", appContext.packageName)
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/LiveDataTestUtil.kt
================================================
package com.vishalgaur.shoppingapp
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun LiveData.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/MainCoroutineRule.kt
================================================
package com.vishalgaur.shoppingapp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import kotlin.coroutines.ContinuationInterceptor
@ExperimentalCoroutinesApi
class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/RecyclerViewMatcherUtils.kt
================================================
package com.vishalgaur.shoppingapp
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.`is`
fun atPosition(position: Int, itemMatcher: Matcher): Matcher {
return object : BoundedMatcher(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position)
?: // has no item on such position
return false
return itemMatcher.matches(viewHolder.itemView)
}
}
}
abstract class RecyclerViewItemAction: ViewAction {
override fun getConstraints(): Matcher? {
return null
}
override fun getDescription(): String {
return "Action on a specific Button"
}
}
class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion {
override fun check(view: View, noViewFoundException: NoMatchingViewException?) {
if (noViewFoundException != null) {
throw noViewFoundException
}
val recyclerView = view as RecyclerView
val adapter = recyclerView.adapter
assertThat(adapter!!.itemCount, `is`(expectedCount))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeAuthRepository.kt
================================================
package com.vishalgaur.shoppingapp.data.source
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.utils.EmailMobileData
import com.vishalgaur.shoppingapp.data.utils.SignUpErrors
import com.vishalgaur.shoppingapp.data.utils.UserType
class FakeAuthRepository(private val sessionManager: ShoppingAppSessionManager) :
AuthRepoInterface {
private var emailMobileData = EmailMobileData()
private var uData: UserData? = null
override suspend fun refreshData() {
// no implementation
}
override suspend fun signUp(userData: UserData) {
uData = userData
sessionManager.createLoginSession(
userData.userId,
userData.name,
userData.mobile,
false,
userData.userType == UserType.SELLER.name
)
}
override fun login(userData: UserData, rememberMe: Boolean) {
uData = userData
sessionManager.createLoginSession(
userData.userId,
userData.name,
userData.mobile,
rememberMe,
userData.userType == UserType.SELLER.name
)
}
override suspend fun checkEmailAndMobile(
email: String,
mobile: String,
context: Context
): SignUpErrors {
// no implementation
return SignUpErrors.NONE
}
override suspend fun checkLogin(mobile: String, password: String): UserData? {
uData?.let {
if (it.mobile == mobile && it.password == password) {
return it
}
}
return null
}
override suspend fun signOut() {
uData = null
sessionManager.logoutFromSession()
}
override suspend fun hardRefreshUserData() {
// no implementation
}
override suspend fun insertProductToLikes(productId: String, userId: String): Result {
uData?.let {
if (it.userId == userId) {
val likes = it.likes.toMutableList()
likes.add(productId)
it.likes = likes
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun removeProductFromLikes(
productId: String,
userId: String
): Result {
uData?.let {
if (it.userId == userId) {
val likes = it.likes.toMutableList()
likes.remove(productId)
it.likes = likes
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun insertAddress(
newAddress: UserData.Address,
userId: String
): Result {
uData?.let {
if (it.userId == userId) {
val addresses = it.addresses.toMutableList()
addresses.add(newAddress)
it.addresses = addresses
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun updateAddress(
newAddress: UserData.Address,
userId: String
): Result {
uData?.let {
if (it.userId == userId) {
val addresses = it.addresses.toMutableList()
addresses.add(newAddress)
val pos =
it.addresses.indexOfFirst { address -> address.addressId == newAddress.addressId }
if (pos >= 0) {
addresses[pos] = newAddress
}
it.addresses = addresses
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun deleteAddressById(addressId: String, userId: String): Result {
uData?.let {
if (it.userId == userId) {
val addresses = it.addresses.toMutableList()
val pos = it.addresses.indexOfFirst { address -> address.addressId == addressId }
if (pos >= 0) {
addresses.removeAt(pos)
}
it.addresses = addresses
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun insertCartItemByUserId(
cartItem: UserData.CartItem,
userId: String
): Result {
uData?.let {
if (it.userId == userId) {
val cart = it.cart.toMutableList()
cart.add(cartItem)
it.cart = cart
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun updateCartItemByUserId(
cartItem: UserData.CartItem,
userId: String
): Result {
uData?.let {
if (it.userId == userId) {
val cart = it.cart.toMutableList()
val pos = it.cart.indexOfFirst { item -> item.itemId == cartItem.itemId }
if (pos >= 0) {
cart[pos] = cartItem
}
it.cart = cart
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result {
uData?.let {
if (it.userId == userId) {
val cart = it.cart.toMutableList()
val pos = it.cart.indexOfFirst { item -> item.itemId == itemId }
if (pos >= 0) {
cart.removeAt(pos)
}
it.cart = cart
return Result.Success(true)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun getAddressesByUserId(userId: String): Result?> {
uData?.let {
if (it.userId == userId) {
return Result.Success(it.addresses)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun getLikesByUserId(userId: String): Result?> {
uData?.let {
if (it.userId == userId) {
return Result.Success(it.likes)
}
}
return Result.Error(Exception("User Not Found"))
}
override suspend fun getUserData(userId: String): Result {
uData?.let {
if (it.userId == userId) {
return Result.Success(it)
}
}
return Result.Error(Exception("User Not Found"))
}
override fun getFirebaseAuth(): FirebaseAuth {
return Firebase.auth
}
override fun signInWithPhoneAuthCredential(
credential: PhoneAuthCredential,
isUserLoggedIn: MutableLiveData,
context: Context
) {
// no implementation
}
override fun isRememberMeOn(): Boolean {
// no implementation
return true
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeProductsDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
class FakeProductsDataSource(private var products: MutableList? = mutableListOf()) :
ProductDataSource {
private val imagesStorage = mutableListOf()
override fun observeProducts(): LiveData>?> {
products?.let { pros ->
val res = MutableLiveData(pros)
return Transformations.map(res) {
Success(it.toList())
}
}
return MutableLiveData(Error(Exception()))
}
override fun observeProductsByOwner(ownerId: String): LiveData>?> {
products?.let { allPros ->
val pros = allPros.filter { pr -> pr.owner == ownerId }
val res = MutableLiveData(pros)
return Transformations.map(res) {
Success(it.toList())
}
}
return MutableLiveData(Error(Exception()))
}
override suspend fun getAllProducts(): Result> {
products?.let {
return Success(it)
}
return Error(Exception("Products Not Found"))
}
override suspend fun refreshProducts() {
// No implementation
}
override suspend fun getProductById(productId: String): Result {
products?.let {
val res = it.filter { product -> product.productId == productId }
return if (res.isNotEmpty()) {
Success(res[0])
} else {
Error(Exception("Product Not Found"))
}
}
return Error(Exception("Product Not Found"))
}
override suspend fun insertProduct(newProduct: Product) {
products?.add(newProduct)
}
override suspend fun updateProduct(proData: Product) {
products?.let {
val pos = it.indexOfFirst { product -> proData.productId == product.productId }
it[pos] = proData
}
}
override suspend fun deleteProduct(productId: String) {
products?.let {
val pos = it.indexOfFirst { product -> productId == product.productId }
if (pos >= 0)
it.removeAt(pos)
else throw Exception("Product Not Found")
}
}
override suspend fun getAllProductsByOwner(ownerId: String): Result> {
val res = products?.filter { product ->
product.owner == ownerId
}
return if (res != null) {
Success(res)
} else {
Success(emptyList())
}
}
override suspend fun deleteAllProducts() {
products = mutableListOf()
}
override suspend fun insertMultipleProducts(data: List) {
products?.addAll(data)
}
override suspend fun uploadImage(uri: Uri, fileName: String): Uri {
val res = uri.toString() + fileName
if (res.contains("invalidinvalidinvalid")) {
throw Exception("Error uploading Images")
}
imagesStorage.add(res)
return res.toUri()
}
override fun revertUpload(fileName: String) {
val pos = imagesStorage.indexOfFirst { imageRef ->
imageRef.contains(fileName)
}
if (pos >= 0) {
imagesStorage.removeAt(pos)
}
}
override fun deleteImage(imgUrl: String) {
imagesStorage.remove(imgUrl)
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeProductsRepository.kt
================================================
package com.vishalgaur.shoppingapp.data.source
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import kotlinx.coroutines.runBlocking
import java.util.*
import kotlin.collections.LinkedHashMap
class FakeProductsRepository : ProductsRepoInterface {
var productsServiceData: LinkedHashMap = LinkedHashMap()
private val imagesStorage = mutableListOf()
private val observableProducts = MutableLiveData>>()
override suspend fun refreshProducts(): StoreDataStatus {
observableProducts.value = Success(productsServiceData.values.toList())
return StoreDataStatus.DONE
}
override fun observeProducts(): LiveData>?> {
runBlocking { refreshProducts() }
return observableProducts
}
override fun observeProductsByOwner(ownerId: String): LiveData>?> {
runBlocking { refreshProducts() }
return Transformations.map(observableProducts) { products ->
when (products) {
is Result.Loading -> Result.Loading
is Error -> Error(products.exception)
is Success -> {
val pros = products.data.filter { it.owner == ownerId }
Success(pros)
}
}
}
}
override suspend fun getAllProductsByOwner(ownerId: String): Result> {
productsServiceData.values.let { pros ->
val res = pros.filter { it.owner == ownerId }
return Success(res)
}
}
override suspend fun getProductById(productId: String, forceUpdate: Boolean): Result {
productsServiceData[productId]?.let {
return Success(it)
}
return Error(Exception("Product Not Found!"))
}
override suspend fun insertProduct(newProduct: Product): Result {
productsServiceData[newProduct.productId] = newProduct
return Success(true)
}
override suspend fun insertImages(imgList: List): List {
val result = mutableListOf()
imgList.forEach { uri ->
val uniId = UUID.randomUUID().toString()
val fileName = uniId + uri.lastPathSegment?.split("/")?.last()
val res = uri.toString() + fileName
imagesStorage.add(res)
result.add(res)
}
return result
}
override suspend fun updateProduct(product: Product): Result {
productsServiceData[product.productId] = product
return Success(true)
}
override suspend fun updateImages(newList: List, oldList: List): List {
val urlList = mutableListOf()
newList.forEach { uri ->
if (!oldList.contains(uri.toString())) {
val uniId = UUID.randomUUID().toString()
val fileName = uniId + uri.lastPathSegment?.split("/")?.last()
val res = uri.toString() + fileName
imagesStorage.add(res)
urlList.add(res)
} else {
urlList.add(uri.toString())
}
}
oldList.forEach { imgUrl ->
if (!newList.contains(imgUrl.toUri())) {
imagesStorage.remove(imgUrl)
}
}
return urlList
}
override suspend fun deleteProductById(productId: String): Result {
productsServiceData.remove(productId)
refreshProducts()
return Success(true)
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/FakeUserDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.EmailMobileData
class FakeUserDataSource(private var uData: UserData?) : UserDataSource {
private var emailMobileData = EmailMobileData()
override suspend fun addUser(userData: UserData) {
uData = userData
}
override suspend fun getUserById(userId: String): Result {
uData?.let {
if (it.userId == userId) {
return Success(it)
}
}
return Error(Exception("User Not Found"))
}
override suspend fun getEmailsAndMobiles(): EmailMobileData {
return emailMobileData
}
override suspend fun getUserByMobileAndPassword(
mobile: String,
password: String
): MutableList {
val res = mutableListOf()
uData?.let {
if (it.mobile == mobile && it.password == password) {
res.add(it)
}
}
return res
}
override suspend fun clearUser() {
uData = null
}
override suspend fun getUserByMobile(phoneNumber: String): UserData? {
return super.getUserByMobile(phoneNumber)
}
override fun updateEmailsAndMobiles(email: String, mobile: String) {
emailMobileData.emails.add(email)
emailMobileData.mobiles.add(mobile)
}
override suspend fun likeProduct(productId: String, userId: String) {
uData?.let {
if (it.userId == userId) {
val likes = it.likes.toMutableList()
likes.add(productId)
it.likes = likes
}
}
}
override suspend fun dislikeProduct(productId: String, userId: String) {
uData?.let {
if (it.userId == userId) {
val likes = it.likes.toMutableList()
likes.remove(productId)
it.likes = likes
}
}
}
override suspend fun insertAddress(newAddress: UserData.Address, userId: String) {
uData?.let {
if (it.userId == userId) {
val addresses = it.addresses.toMutableList()
addresses.add(newAddress)
it.addresses = addresses
}
}
}
override suspend fun updateAddress(newAddress: UserData.Address, userId: String) {
uData?.let { data ->
if (data.userId == userId) {
val addresses = data.addresses.toMutableList()
val pos = data.addresses.indexOfFirst { it.addressId == newAddress.addressId }
if (pos >= 0) {
addresses[pos] = newAddress
}
data.addresses = addresses
}
}
}
override suspend fun deleteAddress(addressId: String, userId: String) {
uData?.let { data ->
if (data.userId == userId) {
val addresses = data.addresses.toMutableList()
val pos = data.addresses.indexOfFirst { it.addressId == addressId }
if (pos >= 0) {
addresses.removeAt(pos)
}
data.addresses = addresses
}
}
}
override suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) {
uData?.let {
if (it.userId == userId) {
val cart = it.cart.toMutableList()
cart.add(newItem)
it.cart = cart
}
}
}
override suspend fun updateCartItem(item: UserData.CartItem, userId: String) {
uData?.let { data ->
if (data.userId == userId) {
val cart = data.cart.toMutableList()
val pos = data.cart.indexOfFirst { it.itemId == item.itemId }
if (pos >= 0) {
cart[pos] = item
}
data.cart = cart
}
}
}
override suspend fun deleteCartItem(itemId: String, userId: String) {
uData?.let { data ->
if (data.userId == userId) {
val cart = data.cart.toMutableList()
val pos = data.cart.indexOfFirst { it.itemId == itemId }
if (pos >= 0) {
cart.removeAt(pos)
}
data.cart = cart
}
}
}
override suspend fun getAddressesByUserId(userId: String): Result?> {
uData?.let {
if (it.userId == userId) {
return Success(it.addresses)
}
}
return Error(Exception("User Not Found"))
}
override suspend fun getLikesByUserId(userId: String): Result?> {
uData?.let {
if (it.userId == userId) {
return Success(it.likes)
}
}
return Error(Exception("User Not Found"))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/repository/AuthRepositoryTest.kt
================================================
package com.vishalgaur.shoppingapp.data.source.repository
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.google.firebase.FirebaseApp
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.FakeUserDataSource
import com.vishalgaur.shoppingapp.data.utils.SignUpErrors
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.nullValue
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class AuthRepositoryTest {
private val userSeller = UserData(
"weoifhwenf29385",
"Seller Name",
"+919999990000",
"somemail@mail.com",
"12345",
emptyList(),
emptyList(),
emptyList(),
"SELLER",
)
private val userCustomer = UserData(
"dwoeihwjklvn48329752",
"Customer Name",
"+919090909090",
"somemail1232@mail.com",
"12345",
emptyList(),
emptyList(),
emptyList(),
"CUSTOMER",
)
private lateinit var context: Context
private lateinit var userLocalDataSource: FakeUserDataSource
private lateinit var authRemoteDataSource: FakeUserDataSource
private lateinit var sessionManager: ShoppingAppSessionManager
// class under test
private lateinit var authRepository: AuthRepository
@Before
fun createRepository() {
context = ApplicationProvider.getApplicationContext()
FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext())
userLocalDataSource = FakeUserDataSource(userSeller)
authRemoteDataSource = FakeUserDataSource(userCustomer)
sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext())
authRepository = AuthRepository(
userLocalDataSource,
authRemoteDataSource,
sessionManager
)
}
@Test
fun login_getUserDetailFromSession() = runBlockingTest {
authRepository.login(userSeller, true)
val result = sessionManager.getUserDataFromSession()
assertThat(result["userName"], `is`(userSeller.name))
assertThat(result["userId"], `is`(userSeller.userId))
assertThat(result["userMobile"], `is`(userSeller.mobile))
}
@Test
fun singUp_addsUserToSources() = runBlockingTest {
authRepository.signUp(userCustomer)
val resultSession = sessionManager.getUserDataFromSession()
assertThat(resultSession["userName"], `is`(userCustomer.name))
assertThat(resultSession["userId"], `is`(userCustomer.userId))
assertThat(resultSession["userMobile"], `is`(userCustomer.mobile))
val localRes = userLocalDataSource.getUserById(userCustomer.userId)
assertThat(localRes, `is`(Success(userCustomer)))
val remoteRes = authRemoteDataSource.getUserById(userCustomer.userId)
assertThat(remoteRes, `is`(Success(userCustomer)))
}
@Test
fun checkEmailAndMobile_existingEmail_returnsError() {
authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888")
runOnUiThread {
runBlockingTest {
val result =
authRepository.checkEmailAndMobile("mail123@mail.com", "+919685", context)
assertThat(result, `is`(SignUpErrors.SERR))
}
}
}
@Test
fun checkEmailAndMobile_existingMobile_returnsError() {
authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888")
runOnUiThread {
runBlockingTest {
val result =
authRepository.checkEmailAndMobile("mail999@mail.com", "+919999988888", context)
assertThat(result, `is`(SignUpErrors.SERR))
}
}
}
@Test
fun checkEmailAndMobile_existingMobileAndEmail_returnsError() {
authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888")
runOnUiThread {
runBlockingTest {
val result =
authRepository.checkEmailAndMobile("mail123@mail.com", "+919999988888", context)
assertThat(result, `is`(SignUpErrors.SERR))
}
}
}
@Test
fun checkEmailAndMobile_newData_returnsError() {
authRemoteDataSource.updateEmailsAndMobiles("mail123@mail.com", "+919999988888")
runOnUiThread {
runBlockingTest {
val result =
authRepository.checkEmailAndMobile(
"somemail123@mail.com",
"+919999977777",
context
)
assertThat(result, `is`(SignUpErrors.NONE))
}
}
}
@Test
fun checkLogin_existingUser_returnsData() = runBlockingTest {
val result = authRepository.checkLogin(userCustomer.mobile, userCustomer.password)
assertThat(result, `is`(userCustomer))
}
@Test
fun checkLogin_newCredentials_returnsNull() = runBlockingTest {
val result = authRepository.checkLogin("+919879879879", "sdygt4")
assertThat(result, `is`(nullValue()))
}
@Test
fun signOut_clearsSessionAndData() = runBlockingTest {
authRepository.signOut()
val sessionRes = sessionManager.isLoggedIn()
val localRes = userLocalDataSource.getUserById(userSeller.userId)
assertThat(sessionRes, `is`(false))
if (localRes is Success)
assert(false)
else if (localRes is Error) {
assertEquals(localRes.exception.message, "User Not Found")
}
}
@Test
fun getLikes_returnsLikes() = runBlockingTest {
authRepository.signUp(userCustomer)
val res = authRepository.getLikesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data, `is`(userCustomer.likes))
} else {
assert(false)
}
}
@Test
fun likeProduct() = runBlockingTest {
authRepository.signUp(userCustomer)
authRepository.insertProductToLikes("some-id", userCustomer.userId)
val res = authRepository.getLikesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data?.size, `is`(1))
} else {
assert(false)
}
}
@Test
fun dislikeProduct() = runBlockingTest {
authRepository.signUp(userCustomer)
authRepository.insertProductToLikes("some-id", userCustomer.userId)
authRepository.removeProductFromLikes("some-id", userCustomer.userId)
val res = authRepository.getLikesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data?.contains("some-id"), `is`(false))
} else {
assert(false)
}
}
@Test
fun getAddresses() = runBlockingTest {
authRepository.signUp(userCustomer)
val res = authRepository.getAddressesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data, `is`(userCustomer.addresses))
} else {
assert(false)
}
}
@Test
fun addAddress() = runBlockingTest {
authRepository.signUp(userCustomer)
val address = UserData.Address(
"id123-add",
"namefirst",
"lname",
"IN",
"2341 weg",
"",
"kanopwe",
"up",
"209876",
"+919999988888"
)
authRepository.insertAddress(address, userCustomer.userId)
val res = authRepository.getAddressesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data?.size, `is`(1))
} else {
assert(false)
}
}
@Test
fun removeAddress() = runBlockingTest {
authRepository.signUp(userCustomer)
val address = UserData.Address(
"id123-add",
"namefirst",
"lname",
"IN",
"2341 weg",
"",
"kanopwe",
"up",
"209876",
"+919999988888"
)
authRepository.insertAddress(address, userCustomer.userId)
authRepository.deleteAddressById(address.addressId, userCustomer.userId)
val res = authRepository.getAddressesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data?.contains(address), `is`(false))
} else {
assert(false)
}
}
@Test
fun updateAddress() = runBlockingTest {
authRepository.signUp(userCustomer)
val address = UserData.Address(
"id123-add",
"namefirst",
"lname",
"IN",
"2341 weg",
"",
"kanopwe",
"up",
"209876",
"+919999988888"
)
val newAddress = UserData.Address(
"id123-add",
"namefirst",
"lname",
"IN",
"2341 wesfgeg",
"vdfg, heth",
"kanopwe",
"up",
"209876",
"+919999988888"
)
authRepository.insertAddress(address, userCustomer.userId)
authRepository.updateAddress(newAddress, userCustomer.userId)
val res = authRepository.getAddressesByUserId(userCustomer.userId)
if (res is Success) {
assertThat(res.data?.contains(newAddress), `is`(true))
} else {
assert(false)
}
}
@Test
fun addItemToCart() = runBlockingTest {
authRepository.signUp(userCustomer)
val item = UserData.CartItem(
"item-id-123",
"pro-122",
"owner-1213",
1,
"black",
7
)
authRepository.insertCartItemByUserId(item, userCustomer.userId)
val userRes = userLocalDataSource.getUserById(userCustomer.userId)
if (userRes is Success) {
val data = userRes.data
if (data != null) {
val cart = data.cart
assertThat(cart.contains(item), `is`(true))
} else {
assert(false)
}
} else {
assert(false)
}
}
@Test
fun removeItemFromCart() = runBlockingTest {
authRepository.signUp(userCustomer)
val item = UserData.CartItem(
"item-id-123",
"pro-122",
"owner-1213",
1,
"black",
7
)
authRepository.insertCartItemByUserId(item, userCustomer.userId)
authRepository.deleteCartItemByUserId(item.itemId, userCustomer.userId)
val userRes = userLocalDataSource.getUserById(userCustomer.userId)
if (userRes is Success) {
val data = userRes.data
if (data != null) {
val cart = data.cart
assertThat(cart.contains(item), `is`(false))
} else {
assert(false)
}
} else {
assert(false)
}
}
@Test
fun updateItemInCart() = runBlockingTest {
authRepository.signUp(userCustomer)
val item = UserData.CartItem(
"item-id-123",
"pro-122",
"owner-1213",
1,
"black",
7
)
val newItem = UserData.CartItem(
"item-id-123",
"pro-122",
"owner-1213",
5,
"black",
7
)
authRepository.insertCartItemByUserId(item, userCustomer.userId)
authRepository.updateCartItemByUserId(newItem, userCustomer.userId)
val userRes = userLocalDataSource.getUserById(userCustomer.userId)
if (userRes is Success) {
val data = userRes.data
if (data != null) {
val cart = data.cart
assertThat(cart.contains(newItem), `is`(true))
} else {
assert(false)
}
} else {
assert(false)
}
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/data/source/repository/ProductsRepositoryTest.kt
================================================
package com.vishalgaur.shoppingapp.data.source.repository
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.core.net.toUri
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import com.vishalgaur.shoppingapp.ERR_UPLOAD
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.source.FakeProductsDataSource
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.getOrAwaitValue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.greaterThan
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ExperimentalCoroutinesApi
class ProductsRepositoryTest {
private val pro1 = Product(
"pro-owner1-shoe-101",
"Shoe Name 101",
"owner1",
"some description",
"Shoes",
250.0,
300.0,
listOf(5, 6, 7, 8),
listOf("Red", "Blue"),
listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"),
2.5
)
private val pro2 = Product(
"pro-owner1-slipper-101",
"Slipper Name 101",
"owner1",
"some description",
"Slippers",
50.0,
80.0,
listOf(6, 7, 8),
listOf("Black", "Blue"),
listOf(
"http://image-ref-uri/-slipper-101-01.jpg",
"http://image-ref-uri/-slipper-101-02.jpg"
),
4.0
)
private val pro3 = Product(
"pro-owner1-shoe-102",
"Shoe Name 102",
"owner2",
"some description",
"Shoes",
450.0,
600.0,
listOf(4, 5, 7, 8, 10),
listOf("Red", "Blue", "White"),
listOf("http://image-ref-uri/-shoe-102-01.jpg", "http://image-ref-uri/-shoe-102-02.jpg"),
3.5
)
private lateinit var productsLocalDataSource: FakeProductsDataSource
private lateinit var productsRemoteDataSource: FakeProductsDataSource
// class under test
private lateinit var productsRepository: ProductsRepository
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun createRepository() {
productsLocalDataSource = FakeProductsDataSource(mutableListOf())
productsRemoteDataSource = FakeProductsDataSource(mutableListOf(pro1, pro3))
productsRepository = ProductsRepository(productsRemoteDataSource, productsLocalDataSource)
}
@Test
fun getProductsById_invalidId_returnsError() = runBlockingTest {
val resultRes = productsRepository.getProductById("invalidId", false)
if (resultRes is Result.Success)
assert(false)
else if (resultRes is Result.Error) {
assertEquals(resultRes.exception.message, "Product Not Found")
}
}
@Test
fun getProductsById_validId_returnsProduct() = runBlockingTest {
productsRepository.insertProduct(pro1)
val resultRes = productsRepository.getProductById(pro1.productId, false)
if (resultRes is Result.Success) {
assertThat(resultRes.data, `is`(pro1))
} else if (resultRes is Result.Error) {
assert(false)
}
}
@Test
fun insertProduct_returnsSuccess() = runBlockingTest {
val insertRes = productsRepository.insertProduct(pro1)
if (insertRes is Result.Success) {
assertThat(insertRes.data, `is`(true))
} else {
assert(false)
}
}
@Test
fun insertImages_returnsSuccess() = runBlockingTest {
val result = productsRepository.insertImages(pro1.images.map { it.toUri() })
assertThat(result.size, `is`(pro1.images.size))
}
@Test
fun insertImages_invalidImages_returnsError() = runBlockingTest {
val result =
productsRepository.insertImages(listOf("http://image-ref-uri/dwoeiovnwi-invalidinvalidinvalid/weoifhowf".toUri()))
assertThat(result[0], `is`(ERR_UPLOAD))
}
@Test
fun updateProduct_returnsSuccess() = runBlockingTest {
productsRepository.insertProduct(pro2)
val updatedPro = pro2
updatedPro.availableSizes = listOf(5, 6, 10, 12)
val insertRes = productsRepository.updateProduct(updatedPro)
if (insertRes is Result.Success) {
assertThat(insertRes.data, `is`(true))
} else {
assert(false)
}
}
@Test
fun updateImages_returnsList() = runBlockingTest {
val oldList = productsRepository.insertImages(pro1.images.map { it.toUri() })
val result = productsRepository.updateImages(pro3.images.map { it.toUri() }, oldList)
assertThat(result.size, `is`(pro3.images.size))
}
@Test
fun updateImages_invalidImage_returnsError() = runBlockingTest {
val oldList = productsRepository.insertImages(pro1.images.map { it.toUri() })
val newList = oldList.toMutableList()
newList[0] = "http://csifduoskjgn/invalidinvalidinvalid/wehoiw"
val result = productsRepository.updateImages(newList.map { it.toUri() }, oldList)
assertThat(result[0], `is`(ERR_UPLOAD))
}
@Test
fun deleteProductById_returnsSuccess() = runBlockingTest {
productsRepository.insertProduct(pro1)
productsRepository.insertProduct(pro2)
val result = productsRepository.deleteProductById(pro1.productId)
assert(result is Result.Success)
}
@Test
fun deleteProductById_invalidId_returnsError() = runBlockingTest {
productsRepository.insertProduct(pro1)
productsRepository.insertProduct(pro2)
val result = productsRepository.deleteProductById(pro3.productId)
assert(result is Result.Error)
}
@Test
fun refreshProducts_returnsSuccess() = runBlockingTest {
val result = productsRepository.refreshProducts()
assertThat(result, `is`(StoreDataStatus.DONE))
}
@Test
fun observeProducts_noData_returnsNoData() = runBlockingTest {
productsLocalDataSource.deleteAllProducts()
val result = productsRepository.observeProducts().getOrAwaitValue()
if (result is Result.Success) {
assertThat(result.data.size, `is`(0))
} else {
assert(false)
}
}
@Test
fun observeProducts_hasData_returnsSuccessWithData() = runBlockingTest {
val initialValue = productsRepository.observeProducts().getOrAwaitValue()
val insertRes = async { productsRepository.insertProduct(pro3) }
insertRes.await()
val refreshRes = async { productsRepository.refreshProducts() }
assertThat(refreshRes.await(), `is`(StoreDataStatus.DONE))
val newValue = productsRepository.observeProducts().getOrAwaitValue()
assertNotEquals(initialValue.toString(), newValue.toString())
if (initialValue is Result.Success) {
assertThat(initialValue.data.size, `is`(0))
} else {
assert(false)
}
if (newValue is Result.Success) {
assertThat(newValue.data.size, `is`(greaterThan(0)))
} else {
assert(false)
}
}
@Test
fun observeProductsByOwner_noData_returnsNoData() = runBlockingTest {
productsLocalDataSource.deleteAllProducts()
val result = productsRepository.observeProductsByOwner(pro1.owner).getOrAwaitValue()
if (result is Result.Success) {
assertThat(result.data.size, `is`(0))
} else {
assert(false)
}
}
@Test
fun observeProductsByOwner_hasData_returnsSuccessWithData() = runBlockingTest {
val initialValue = productsRepository.observeProductsByOwner(pro3.owner).getOrAwaitValue()
val insertRes = async { productsRepository.insertProduct(pro3) }
insertRes.await()
val refreshRes = async { productsRepository.refreshProducts() }
assertThat(refreshRes.await(), `is`(StoreDataStatus.DONE))
val newValue = productsRepository.observeProductsByOwner(pro3.owner).getOrAwaitValue()
assertNotEquals(initialValue.toString(), newValue.toString())
if (initialValue is Result.Success) {
assertThat(initialValue.data.size, `is`(0))
} else {
assert(false)
}
if (newValue is Result.Success) {
assertThat(newValue.data.size, `is`(greaterThan(0)))
} else {
assert(false)
}
}
@Test
fun getAllProductsByOwner_noData_returnsNoData() = runBlockingTest {
productsLocalDataSource.deleteAllProducts()
val result = productsRepository.getAllProductsByOwner(pro1.owner)
if (result is Result.Success) {
assertThat(result.data.size, `is`(0))
} else {
assert(false)
}
}
@Test
fun getAllProductsByOwner_hasData_returnsData() = runBlockingTest {
productsRepository.refreshProducts()
val result = productsRepository.getAllProductsByOwner(pro1.owner)
if (result is Result.Success) {
assertThat(result.data.size, `is`(greaterThan(0)))
} else {
assert(false)
}
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/home/HomeFragmentTest.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.core.view.isVisible
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.testing.TestNavHostController
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.vishalgaur.shoppingapp.*
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository
import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.`is`
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class HomeFragmentTest {
private lateinit var homeScenario: FragmentScenario
private lateinit var navController: NavController
private lateinit var sessionManager: ShoppingAppSessionManager
private lateinit var productsRepository: ProductsRepoInterface
private lateinit var authRepository: AuthRepoInterface
private val context = ApplicationProvider.getApplicationContext()
private val userCustomer = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
ArrayList(),
ArrayList(),
ArrayList(),
"CUSTOMER"
)
private val userSeller = UserData(
"user1234selller",
"Some Name",
"+919999988888",
"somemail123seller@somemail.com",
"1234",
emptyList(),
emptyList(),
emptyList(),
"SELLER"
)
private val pro1 = Product(
"pro-owner1-shoe-101",
"Shoe Name 101",
"user1234selller",
"some description",
"Shoes",
250.0,
300.0,
listOf(5, 6, 7, 8),
listOf("Red", "Blue"),
listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"),
2.5
)
private val pro2 = Product(
"pro-owner1-slipper-101",
"Slipper Name 101",
"owner1",
"some description",
"Slippers",
50.0,
80.0,
listOf(6, 7, 8),
listOf("Black", "Blue"),
listOf(
"http://image-ref-uri/-slipper-101-01.jpg",
"http://image-ref-uri/-slipper-101-02.jpg"
),
4.0
)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
sessionManager = ShoppingAppSessionManager(context)
authRepository = FakeAuthRepository(sessionManager)
productsRepository = FakeProductsRepository()
ServiceLocator.productsRepository = productsRepository
}
@After
fun cleanUp() = runBlockingTest {
authRepository.signOut()
ServiceLocator.resetRepository()
}
@Test
fun userCustomer_hideFABandEditDeleteButtons() = runBlockingTest {
insertProducts()
loginCustomer()
onView(withId(R.id.home_fab_add_product)).check(matches(withEffectiveVisibility(Visibility.GONE)))
//testing recyclerview items
onView(withId(R.id.products_recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition(
0,
object : RecyclerViewItemAction() {
override fun perform(uiController: UiController?, view: View) {
val editButton: ImageView = view.findViewById(R.id.product_edit_button)
val deleteButton: ImageView =
view.findViewById(R.id.product_delete_button)
assertThat(editButton.isVisible, `is`(false))
assertThat(deleteButton.isVisible, `is`(false))
}
})
)
}
@Test
fun userSeller_showFABandEditDeleteButtons() = runBlockingTest {
insertProducts()
loginSeller()
onView(withId(R.id.home_fab_add_product)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
//testing recyclerview items
onView(withId(R.id.products_recycler_view))
.perform(
RecyclerViewActions.actionOnItemAtPosition(
0,
object : RecyclerViewItemAction() {
override fun perform(uiController: UiController?, view: View) {
val editButton: ImageView = view.findViewById(R.id.product_edit_button)
val deleteButton: ImageView =
view.findViewById(R.id.product_delete_button)
assertThat(editButton.isVisible, `is`(true))
assertThat(deleteButton.isVisible, `is`(true))
}
})
)
}
@Test
fun onFABClick_openCategoryDialog() {
loginSeller()
onView(withId(R.id.home_fab_add_product)).perform(click())
onView(withText(R.string.pro_cat_dialog_title)).inRoot(isDialog())
.check(matches(isDisplayed()))
}
@Test
fun onSelectCategory_openAddProductFragment() {
loginSeller()
onView(withId(R.id.home_fab_add_product)).perform(click())
onView(withText(R.string.pro_cat_dialog_ok_btn)).inRoot(isDialog())
.check(matches(isDisplayed())).perform(click())
assertThat(navController.currentDestination?.id, `is`(R.id.addEditProductFragment))
}
@Test
fun onFilterClick_openFilterDialog() = runBlockingTest{
insertProducts()
loginCustomer()
onView(withId(R.id.home_filter)).perform(click())
onView(withText("Shoes")).inRoot(isDialog())
.perform(click())
onView(withText(R.string.pro_cat_dialog_ok_btn)).inRoot(isDialog())
.perform(click())
onView(withId(R.id.products_recycler_view)).check(RecyclerViewItemCountAssertion(1))
}
@Test
fun onFilter_filterResults() {
loginCustomer()
onView(withId(R.id.home_filter)).perform(click())
onView(withText(R.string.pro_cat_dialog_title)).inRoot(isDialog())
.check(matches(isDisplayed()))
}
@Test
fun enterSearch_filtersResults() {
runBlocking {
insertProducts()
loginCustomer()
onView(withId(R.id.home_search_edit_text)).perform(typeText("slipper"))
delay(500)
onView(withId(R.id.products_recycler_view)).check(RecyclerViewItemCountAssertion(1))
}
}
private fun loginSeller() {
authRepository.login(userSeller, true)
ServiceLocator.authRepository = authRepository
setScenarioAndNav()
}
private fun loginCustomer() {
authRepository.login(userCustomer, true)
ServiceLocator.authRepository = authRepository
setScenarioAndNav()
}
private fun setScenarioAndNav() {
homeScenario = launchFragmentInContainer(Bundle(), R.style.Theme_ShoppingApp)
navController = TestNavHostController(context)
runOnUiThread {
navController.setGraph(R.navigation.home_nav_graph)
homeScenario.onFragment {
Navigation.setViewNavController(it.requireView(), navController)
}
}
}
private suspend fun insertProducts() {
productsRepository.insertProduct(pro1)
productsRepository.insertProduct(pro2)
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/home/ProductDetailsFragmentTest.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.fragment.app.testing.FragmentScenario
import androidx.navigation.NavController
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.ServiceLocator
import com.vishalgaur.shoppingapp.ShoppingApplication
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository
import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class ProductDetailsFragmentTest {
private lateinit var productDetailScenario: FragmentScenario
private lateinit var navController: NavController
private lateinit var sessionManager: ShoppingAppSessionManager
private lateinit var productsRepository: ProductsRepoInterface
private lateinit var authRepository: AuthRepoInterface
private val context = ApplicationProvider.getApplicationContext()
private val pro1 = Product(
"pro-owner1-shoe-101",
"Shoe Name 101",
"user1234selller",
"some description",
"Shoes",
250.0,
300.0,
listOf(5, 6, 7, 8),
listOf("Red", "Blue"),
listOf("http://image-ref-uri/shoe-101-01.jpg", "http://image-ref-uri/-shoe-101-02.jpg"),
2.5
)
private val userCustomer = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
ArrayList(),
ArrayList(),
ArrayList(),
"CUSTOMER"
)
private val userSeller = UserData(
"user1234selller",
"Some Name",
"+919999988888",
"somemail123seller@somemail.com",
"1234",
emptyList(),
emptyList(),
emptyList(),
"SELLER"
)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
sessionManager = ShoppingAppSessionManager(context)
authRepository = FakeAuthRepository(sessionManager)
productsRepository = FakeProductsRepository()
ServiceLocator.productsRepository = productsRepository
}
@After
fun cleanUp() = runBlockingTest {
authRepository.signOut()
ServiceLocator.resetRepository()
}
private suspend fun insertProducts() {
productsRepository.insertProduct(pro1)
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/loginSignup/LoginFragmentTest.kt
================================================
package com.vishalgaur.shoppingapp.ui.loginSignup
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.testing.TestNavHostController
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import androidx.test.platform.app.InstrumentationRegistry
import com.vishalgaur.shoppingapp.MOB_ERROR_TEXT
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.clickClickableSpan
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import org.hamcrest.Matchers.`is`
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginFragmentTest {
private lateinit var loginScenario: FragmentScenario
private lateinit var navController: NavController
private lateinit var sessionManager: ShoppingAppSessionManager
@Before
fun setUp() {
sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext())
sessionManager.logoutFromSession()
loginScenario = launchFragmentInContainer(themeResId = R.style.Theme_ShoppingApp)
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
runOnUiThread {
navController.setGraph(R.navigation.signup_nav_graph)
loginScenario.onFragment {
Navigation.setViewNavController(it.requireView(), navController)
}
}
}
@Test
fun useAppContext() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
Assert.assertEquals("com.vishalgaur.shoppingapp", context.packageName)
}
@Test
fun userCanEnterMobile() {
insertInMobileEditText("8976527465")
}
@Test
fun userCanEnterPassword() {
insertInPwdEditText("dh239048fy")
}
@Test
fun userCanClickRemSwitch() {
clickRememberSwitch()
}
@Test
fun userCanClickSignUpText() {
clickSignUpText()
}
@Test
fun userCanClickForgotTextView() {
clickForgotTextView()
}
@Test
fun userCanClickLoginButton() {
clickLoginButton()
}
@Test
fun onSignUpClick_navigateToSignUpFragment() {
clickSignUpText()
assertThat(navController.currentDestination?.id, `is`(R.id.SignupFragment))
}
@Test
fun onLogin_emptyForm_showsError() {
clickLoginButton()
onView(withId(R.id.login_error_text_view)).check(matches(isDisplayed()))
}
@Test
fun onLogin_invalidMobile_showsError() {
insertInMobileEditText(" 467856 ")
insertInPwdEditText("fd3g24")
clickLoginButton()
onView(withId(R.id.login_mobile_edit_text)).check(matches(hasErrorText(`is`(MOB_ERROR_TEXT))))
}
@Test
fun onLogin_validData_showsNoError() {
Intents.init()
insertInMobileEditText("9966339966")
insertInPwdEditText("1234")
clickLoginButton()
intended(hasComponent(OtpActivity::class.java.name))
}
private fun insertInMobileEditText(phone: String) =
onView(withId(R.id.login_mobile_edit_text)).perform(
scrollTo(),
clearText(),
typeText(phone)
)
private fun insertInPwdEditText(pwd: String) =
onView(withId(R.id.login_password_edit_text)).perform(
scrollTo(),
clearText(),
typeText(pwd)
)
private fun clickRememberSwitch() =
onView(withId(R.id.login_rem_switch))
.perform(scrollTo(), click())
private fun clickForgotTextView() =
onView(withId(R.id.login_forgot_tv))
.perform(scrollTo(), click())
private fun clickLoginButton() =
onView(withId(R.id.login_login_btn))
.perform(scrollTo(), click())
private fun clickSignUpText() =
onView(withId(R.id.login_signup_text_view)).perform(
scrollTo(),
clickClickableSpan("Sign Up")
)
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/ui/loginSignup/SignupFragmentTest.kt
================================================
package com.vishalgaur.shoppingapp.ui.loginSignup
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.testing.TestNavHostController
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import androidx.test.platform.app.InstrumentationRegistry
import com.vishalgaur.shoppingapp.EMAIL_ERROR_TEXT
import com.vishalgaur.shoppingapp.MOB_ERROR_TEXT
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.clickClickableSpan
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import org.hamcrest.Matchers.`is`
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SignupFragmentTest {
private lateinit var signUpScenario: FragmentScenario
private lateinit var navController: NavController
private lateinit var sessionManager: ShoppingAppSessionManager
@Before
fun setUp() {
sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext())
sessionManager.logoutFromSession()
signUpScenario = launchFragmentInContainer(themeResId = R.style.Theme_ShoppingApp)
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
runOnUiThread {
navController.setGraph(R.navigation.signup_nav_graph)
signUpScenario.onFragment {
Navigation.setViewNavController(it.requireView(), navController)
}
}
}
@Test
fun useAppContext() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.vishalgaur.shoppingapp", context.packageName)
}
@Test
fun userCanEnterName() {
insertInNameEditText("Vishal Gaur ")
}
@Test
fun userCanEnterMobile() {
insertInMobileEditText("8976527465")
}
@Test
fun userCanEnterEmail() {
insertInEmailEditText(" weuiyjkh@ujhyew.dciwe")
}
@Test
fun userCanEnterPassword() {
insertInPwdEditText("dh239048fy")
}
@Test
fun userCanEnterInCnfPassword() {
insertInCnfPwdEditText("con34uyf98")
}
@Test
fun userCanClickTermsSwitch() {
clickTermsSwitch()
}
@Test
fun userCanClickSellerSwitch() {
clickSellerSwitch()
}
@Test
fun userCanClickSignUp() {
clickSignUpButton()
}
@Test
fun userCanClickLogInText() {
clickLoginText()
}
@Test
fun onLoginClick_navigateToLoginFragment() {
clickLoginText()
assertEquals(navController.currentDestination?.id, R.id.LoginFragment)
}
@Test
fun onSignUp_emptyForm_showsError() {
clickSignUpButton()
onView(withId(R.id.signup_error_text_view)).check(matches(isDisplayed()))
}
@Test
fun onSignUp_invalidEmail_showsEmailError() {
insertInNameEditText("Vishal Gaur ")
insertInMobileEditText("8976527465 ")
insertInEmailEditText(" weuiyjyew.dciwe")
insertInPwdEditText("dh239048fy")
insertInCnfPwdEditText("dh239048fy")
clickTermsSwitch()
clickSignUpButton()
onView(withId(R.id.signup_email_edit_text)).check(matches(hasErrorText(`is`(EMAIL_ERROR_TEXT))))
}
@Test
fun onSignUp_invalidMobile_showsMobileError() {
insertInNameEditText("Vishal Gaur ")
insertInMobileEditText("86527465 ")
insertInEmailEditText(" weuiyj@yew.dciwe")
insertInPwdEditText("dh239048fy")
insertInCnfPwdEditText("dh239048fy")
clickTermsSwitch()
clickSignUpButton()
onView(withId(R.id.signup_mobile_edit_text)).check(matches(hasErrorText(`is`(MOB_ERROR_TEXT))))
}
@Test
fun onSignUp_notAcceptedTerms_showsError() {
insertInNameEditText("Vishal Gaur ")
insertInMobileEditText("8652744565 ")
insertInEmailEditText(" weuiyj@yew.dciwe")
insertInPwdEditText("dh239048fy")
insertInCnfPwdEditText("dh239048fy")
clickSignUpButton()
onView(withId(R.id.signup_error_text_view)).check(matches(withText("Accept the Terms.")))
}
@Test
fun onSignUp_notSamePasswords_showsError() {
insertInNameEditText("Vishal Gaur ")
insertInMobileEditText("8652744565 ")
insertInEmailEditText(" weuiyj@yew.dciwe")
insertInPwdEditText("dh2398fy")
insertInCnfPwdEditText("dh239048fy")
clickTermsSwitch()
clickSignUpButton()
onView(withId(R.id.signup_error_text_view)).check(matches(withText("Both passwords are not same!")))
}
@Test
fun onSignUp_validForm_showsNoError() {
Intents.init()
insertInNameEditText("Vishal Gaur ")
insertInMobileEditText("8652744565 ")
insertInEmailEditText(" weuiyj@yew.dciwe")
insertInPwdEditText("dh2398fy")
insertInCnfPwdEditText("dh2398fy")
clickTermsSwitch()
clickSignUpButton()
intended(hasComponent(OtpActivity::class.java.name))
}
private fun insertInNameEditText(name: String) =
onView(withId(R.id.signup_name_edit_text)).perform(scrollTo(), clearText(), typeText(name))
private fun insertInMobileEditText(phone: String) =
onView(withId(R.id.signup_mobile_edit_text)).perform(
scrollTo(),
clearText(),
typeText(phone)
)
private fun insertInEmailEditText(email: String) =
onView(withId(R.id.signup_email_edit_text)).perform(
scrollTo(),
clearText(),
typeText(email)
)
private fun insertInPwdEditText(pwd: String) =
onView(withId(R.id.signup_password_edit_text)).perform(
scrollTo(),
clearText(),
typeText(pwd)
)
private fun insertInCnfPwdEditText(pwd2: String) =
onView(withId(R.id.signup_cnf_password_edit_text)).perform(
scrollTo(),
clearText(),
typeText(pwd2)
)
private fun clickTermsSwitch() =
onView(withId(R.id.signup_policy_switch)).perform(scrollTo(), click())
private fun clickSellerSwitch() =
onView(withId(R.id.signup_seller_switch)).perform(scrollTo(), click())
private fun clickSignUpButton() =
onView(withId(R.id.signup_signup_btn)).perform(scrollTo(), click())
private fun clickLoginText() =
onView(withId(R.id.signup_login_text_view)).perform(
scrollTo(),
clickClickableSpan("Log In")
)
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/AddEditAddressViewModelTest.kt
================================================
package com.vishalgaur.shoppingapp.viewModels
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.ServiceLocator
import com.vishalgaur.shoppingapp.ShoppingApplication
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.getOrAwaitValue
import com.vishalgaur.shoppingapp.ui.AddAddressViewErrors
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class AddEditAddressViewModelTest {
private lateinit var addEditAddressViewModel: AddEditAddressViewModel
private lateinit var authRepository: AuthRepoInterface
val user = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
ArrayList(),
ArrayList(),
ArrayList()
)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext()
val sessionManager = ShoppingAppSessionManager(context)
authRepository = FakeAuthRepository(sessionManager)
authRepository.login(user, true)
ServiceLocator.authRepository = authRepository
addEditAddressViewModel = AddEditAddressViewModel(context)
}
@After
fun cleanUp() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun setIsEdit_setsValue() {
runBlockingTest {
addEditAddressViewModel.setIsEdit(false)
val res = addEditAddressViewModel.isEdit.getOrAwaitValue()
assertThat(res, `is`(false))
}
}
@Test
fun setAddressData_setsData() = runBlockingTest {
val address = UserData.Address(
"add-id-121",
"adg",
"shgd",
"IN",
"sfhg45eyh",
"",
"kanpuit",
"up",
"309890",
"9999988558"
)
authRepository.insertAddress(address, user.userId)
addEditAddressViewModel.setAddressData(address.addressId)
val result = addEditAddressViewModel.addressData.getOrAwaitValue()
assertThat(result, `is`(address))
}
@Test
fun submitAddress_emptyForm_returnsError() {
val fname = "adg"
val lname = ""
val code = "IN"
val streetAdd = "sfhg45eyh"
val streetAdd2 = ""
val city = ""
val state = "up"
val zip = "309890"
val phone = "9999988558"
addEditAddressViewModel.submitAddress(
code,
fname,
lname,
streetAdd,
streetAdd2,
city,
state,
zip,
phone
)
val result = addEditAddressViewModel.errorStatus.getOrAwaitValue()
assertThat(result.size, `is`(greaterThan(0)))
assertThat(result.contains(AddAddressViewErrors.ERR_CITY_EMPTY), `is`(true))
}
@Test
fun submitAddress_invalidZipcode_returnsError() {
val fname = "adg"
val lname = "serdg"
val code = "IN"
val streetAdd = "sfhg45eyh"
val streetAdd2 = ""
val city = "sfhg"
val state = "up"
val zip = "30990"
val phone = "9999988558"
addEditAddressViewModel.submitAddress(
code,
fname,
lname,
streetAdd,
streetAdd2,
city,
state,
zip,
phone
)
val result = addEditAddressViewModel.errorStatus.getOrAwaitValue()
assertThat(result.size, `is`(greaterThan(0)))
assertThat(result.contains(AddAddressViewErrors.ERR_ZIP_INVALID), `is`(true))
}
@Test
fun submitAddress_invalidPhone_returnsError() {
val fname = "adg"
val lname = "serdg"
val code = "IN"
val streetAdd = "sfhg45eyh"
val streetAdd2 = ""
val city = "sfhg"
val state = "up"
val zip = "309903"
val phone = "9999988efg558"
addEditAddressViewModel.submitAddress(
code,
fname,
lname,
streetAdd,
streetAdd2,
city,
state,
zip,
phone
)
val result = addEditAddressViewModel.errorStatus.getOrAwaitValue()
assertThat(result.size, `is`(greaterThan(0)))
assertThat(result.contains(AddAddressViewErrors.ERR_PHONE_INVALID), `is`(true))
}
@Test
fun submitAddress_validData_returnsNoError() {
val fname = "adg"
val lname = "serdg"
val code = "IN"
val streetAdd = "sfhg45eyh"
val streetAdd2 = ""
val city = "sfhg"
val state = "up"
val zip = "302203"
val phone = "9879988558"
addEditAddressViewModel.submitAddress(
code,
fname,
lname,
streetAdd,
streetAdd2,
city,
state,
zip,
phone
)
val result = addEditAddressViewModel.errorStatus.getOrAwaitValue()
assertThat(result.size, `is`(0))
val resData = addEditAddressViewModel.newAddressData.getOrAwaitValue()
assertThat(resData, `is`(notNullValue()))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/AddEditProductViewModelTest.kt
================================================
package com.vishalgaur.shoppingapp.viewModels
import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.getOrAwaitValue
import com.vishalgaur.shoppingapp.ui.AddProductViewErrors
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.notNullValue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AddEditProductViewModelTest {
private lateinit var addEditProductViewModel: AddEditProductViewModel
private lateinit var sessionManager: ShoppingAppSessionManager
private val userSeller = UserData(
"user1234selller",
"Some Name",
"+919999988888",
"somemail123seller@somemail.com",
"1234",
emptyList(),
emptyList(),
emptyList(),
"SELLER"
)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext())
sessionManager.createLoginSession(
userSeller.userId,
userSeller.name,
userSeller.mobile,
false,
true
)
addEditProductViewModel =
AddEditProductViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun setCategory_Shoes() {
addEditProductViewModel.setCategory("Shoes")
val result = addEditProductViewModel.selectedCategory.getOrAwaitValue()
assertThat(result, `is`("Shoes"))
}
@Test
fun setIsEdit_true() {
addEditProductViewModel.setIsEdit(true)
val result = addEditProductViewModel.isEdit.getOrAwaitValue()
assertThat(result, `is`(true))
}
@Test
fun setIsEdit_false() {
addEditProductViewModel.setIsEdit(false)
val result = addEditProductViewModel.isEdit.getOrAwaitValue()
assertThat(result, `is`(false))
}
@Test
fun submitProduct_noData_returnsEmptyError() {
addEditProductViewModel.setIsEdit(false)
val name = ""
val price = null
val mrp = null
val desc = ""
val sizes = emptyList()
val colors = emptyList()
val imgList = emptyList()
addEditProductViewModel.submitProduct(name, price, mrp, desc, sizes, colors, imgList)
val result = addEditProductViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(AddProductViewErrors.EMPTY))
}
@Test
fun submitProduct_invalidPrice_returnsPriceError() {
addEditProductViewModel.setIsEdit(false)
val name = "vwsf"
val mrp = 125.0
val price = 0.0
val desc = "crw rewg"
val sizes = listOf(5, 6)
val colors = listOf("red", "blue")
val imgList = listOf("ffsd".toUri(), "sws".toUri())
addEditProductViewModel.submitProduct(name, price, mrp, desc, sizes, colors, imgList)
val result = addEditProductViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(AddProductViewErrors.ERR_PRICE_0))
}
@Test
fun submitProduct_allValid_returnsNoError() {
addEditProductViewModel.setIsEdit(false)
addEditProductViewModel.setCategory("Shoes")
val name = "vwsf"
val mrp = 125.0
val price = 100.0
val desc = "crw rewg"
val sizes = listOf(5, 6)
val colors = listOf("red", "blue")
val imgList = listOf("ffsd".toUri(), "sws".toUri())
addEditProductViewModel.submitProduct(name, price, mrp, desc, sizes, colors, imgList)
val result = addEditProductViewModel.errorStatus.getOrAwaitValue()
val resultPro = addEditProductViewModel.newProductData.getOrAwaitValue()
assertThat(result, `is`(AddProductViewErrors.NONE))
assertThat(resultPro, `is`(notNullValue()))
assertThat(resultPro.name, `is`("vwsf"))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/AuthViewModelTest.kt
================================================
package com.vishalgaur.shoppingapp.viewModels
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.getOrAwaitValue
import com.vishalgaur.shoppingapp.ui.LoginViewErrors
import com.vishalgaur.shoppingapp.ui.SignUpViewErrors
import org.hamcrest.Matchers.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AuthViewModelTest {
private lateinit var authViewModel: AuthViewModel
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
authViewModel = AuthViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun signUpSubmitData_noData_returnsEmptyError() {
val name = ""
val mobile = ""
val email = ""
val pwd1 = ""
val pwd2 = ""
val isAccepted = false
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.ERR_EMPTY))
assertThat(authViewModel.userData.value, `is`(nullValue()))
}
@Test
fun signUpSubmitData_notAccepted_returnsNotAccError() {
val name = "uigbivs ihgfdsg"
val mobile = "9988665555"
val email = "owhfoi@oihw.cro"
val pwd1 = "1234"
val pwd2 = "1234"
val isAccepted = false
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.ERR_NOT_ACC))
assertThat(authViewModel.userData.value, `is`(nullValue()))
}
@Test
fun signUpSubmitData_pwdNotEq_returnsPwdError() {
val name = "uigbivs ihgfdsg"
val mobile = "9988665555"
val email = "owhfoi@oihw.cro"
val pwd1 = "12345"
val pwd2 = "1234"
val isAccepted = false
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.ERR_PWD12NS))
assertThat(authViewModel.userData.value, `is`(nullValue()))
}
@Test
fun signUpSubmitData_invalidEmail_returnsEmailError() {
val name = "uigbivs ihgfdsg"
val mobile = "9988665555"
val email = "owhfoi@oihwo"
val pwd1 = "1234"
val pwd2 = "1234"
val isAccepted = true
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.ERR_EMAIL))
assertThat(authViewModel.userData.value, `is`(nullValue()))
}
@Test
fun signUpSubmitData_invalidMobile_returnsMobError() {
val name = "uigbivs ihgfdsg"
val mobile = "9988665fng55"
val email = "owhfoi@oihw.coo"
val pwd1 = "1234"
val pwd2 = "1234"
val isAccepted = true
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.ERR_MOBILE))
assertThat(authViewModel.userData.value, `is`(nullValue()))
}
@Test
fun signUpSubmitData_invalidEmailMobile_returnsEmailMobError() {
val name = "uigbivs ihgfdsg"
val mobile = "9988665fng55"
val email = "owhfoi@oihwoo"
val pwd1 = "1234"
val pwd2 = "1234"
val isAccepted = true
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.ERR_EMAIL_MOBILE))
assertThat(authViewModel.userData.value, `is`(nullValue()))
}
@Test
fun signUpSubmitData_validData_returnsNoError() {
val name = " uigbivs ihgfdsg"
val mobile = " 9988665755"
val email = "owhfoi@oihwoo.cwdo "
val pwd1 = "1234"
val pwd2 = "1234"
val isAccepted = true
val isSeller = false
authViewModel.signUpSubmitData(name, mobile, email, pwd1, pwd2, isAccepted, isSeller)
val result = authViewModel.errorStatus.getOrAwaitValue()
val dataRes = authViewModel.userData.getOrAwaitValue()
assertThat(result, `is`(SignUpViewErrors.NONE))
assertThat(dataRes, `is`(notNullValue()))
assertThat(dataRes.name, `is`("uigbivs ihgfdsg"))
}
@Test
fun loginSubmitData_noData_returnsEmptyError() {
val mobile = ""
val pwd = ""
authViewModel.loginSubmitData(mobile, pwd)
val result = authViewModel.errorStatusLoginFragment.getOrAwaitValue()
assertThat(result, `is`(LoginViewErrors.ERR_EMPTY))
}
@Test
fun loginSubmitData_invalidMobile_returnsMobileError() {
val mobile = "9fwd988556699"
val pwd = "123"
authViewModel.loginSubmitData(mobile, pwd)
val result = authViewModel.errorStatusLoginFragment.getOrAwaitValue()
assertThat(result, `is`(LoginViewErrors.ERR_MOBILE))
}
@Test
fun loginSubmitData_validData_returnsNoError() {
val mobile = "9988556699"
val pwd = "123"
authViewModel.loginSubmitData(mobile, pwd)
val result = authViewModel.errorStatusLoginFragment.getOrAwaitValue()
assertThat(result, `is`(LoginViewErrors.NONE))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/HomeViewModelTest.kt
================================================
package com.vishalgaur.shoppingapp.viewModels
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.ServiceLocator
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.getOrAwaitValue
import org.hamcrest.Matchers.`is`
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class HomeViewModelTest {
private lateinit var homeViewModel: HomeViewModel
private lateinit var authRepository: AuthRepoInterface
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
val sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext())
authRepository = FakeAuthRepository(sessionManager)
ServiceLocator.authRepository = authRepository
homeViewModel = HomeViewModel(ApplicationProvider.getApplicationContext())
}
@After
fun cleanUp() {
ServiceLocator.resetRepository()
}
@Test
fun setDataLoaded_setsValue() {
homeViewModel.setDataLoaded()
val result = homeViewModel.storeDataStatus.getOrAwaitValue()
assertThat(result, `is`(StoreDataStatus.DONE))
}
@Test
fun filterProducts_All() {
homeViewModel.filterProducts("All")
val result = homeViewModel.filterCategory.getOrAwaitValue()
assertThat(result, `is`("All"))
}
@Test
fun filterProducts_Shoes() {
homeViewModel.filterProducts("Shoes")
val result = homeViewModel.filterCategory.getOrAwaitValue()
assertThat(result, `is`("Shoes"))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/OrderViewModelTest.kt
================================================
package com.vishalgaur.shoppingapp.viewModels
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.ServiceLocator
import com.vishalgaur.shoppingapp.ShoppingApplication
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository
import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.getOrAwaitValue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class OrderViewModelTest {
private lateinit var orderViewModel: OrderViewModel
private lateinit var productsRepository: ProductsRepoInterface
private lateinit var authRepository: AuthRepoInterface
val user = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
ArrayList(),
ArrayList(),
ArrayList()
)
val address = UserData.Address(
"add-id-121",
"adg",
"shgd",
"IN",
"sfhg45eyh",
"",
"kanpuit",
"up",
"309890",
"9999988558"
)
val item1 = UserData.CartItem(
"item2123",
"pro123",
"owner23",
1,
"Red",
10
)
val item2 = UserData.CartItem(
"item2123456347",
"pro12345",
"owner23",
1,
"Blue",
9
)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
productsRepository = FakeProductsRepository()
val context = ApplicationProvider.getApplicationContext()
val sessionManager = ShoppingAppSessionManager(context)
authRepository = FakeAuthRepository(sessionManager)
authRepository.login(user, true)
ServiceLocator.productsRepository = productsRepository
ServiceLocator.authRepository = authRepository
orderViewModel = OrderViewModel(context)
}
@After
fun cleanUp() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun getCartItems_loadsData() = runBlocking {
orderViewModel.getCartItems()
delay(200)
val result = orderViewModel.dataStatus.getOrAwaitValue()
assertThat(result, `is`(StoreDataStatus.DONE))
}
@Test
fun getAddresses_noAddress_loadsData() = runBlocking {
orderViewModel.getUserAddresses()
delay(200)
val result = orderViewModel.dataStatus.getOrAwaitValue()
assertThat(result, `is`(StoreDataStatus.DONE))
val resAdd = orderViewModel.userAddresses.getOrAwaitValue()
assertThat(resAdd.size, `is`(0))
}
@Test
fun getAddresses_hasAddress_loadsData() = runBlocking {
authRepository.insertAddress(address, user.userId)
delay(100)
orderViewModel.getUserAddresses()
delay(100)
val result = orderViewModel.dataStatus.getOrAwaitValue()
assertThat(result, `is`(StoreDataStatus.DONE))
val resAdd = orderViewModel.userAddresses.getOrAwaitValue()
assertThat(resAdd.size, `is`(1))
}
@Test
fun deleteAddress_deletesAddress() = runBlocking{
authRepository.insertAddress(address, user.userId)
delay(100)
orderViewModel.getUserAddresses()
delay(100)
val resAdd = orderViewModel.userAddresses.getOrAwaitValue()
assertThat(resAdd.size, `is`(1))
orderViewModel.deleteAddress(address.addressId)
delay(100)
val resAdd2 = orderViewModel.userAddresses.getOrAwaitValue()
assertThat(resAdd2.size, `is`(0))
}
@Test
fun getItemsPriceTotal_returnsTotal() {
runBlocking {
authRepository.insertCartItemByUserId(item1, user.userId)
delay(100)
val result = orderViewModel.getItemsPriceTotal()
assertThat(result, `is`(0.0))
}
}
@Test
fun toggleLike() {
runBlocking {
val res1 = orderViewModel.userLikes.getOrAwaitValue()
orderViewModel.toggleLikeProduct("pro-if2r3")
delay(100)
val res2 = orderViewModel.userLikes.getOrAwaitValue()
assertThat(res1.size, not(res2.size))
}
}
@Test
fun setQuantity_setsQuantity() = runBlocking {
authRepository.insertCartItemByUserId(item1, user.userId)
delay(100)
val res1 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId }
val size1 = res1?.quantity ?: 0
orderViewModel.setQuantityOfItem(item1.itemId, 1)
delay(100)
val res2 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId }
val size2 = res2?.quantity ?: 0
assertThat(size1, `is`(1))
assertThat(size2, `is`(2))
}
@Test
fun deleteItemFromCart_deletesItem() = runBlocking {
authRepository.insertCartItemByUserId(item1, user.userId)
delay(100)
val res1 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId }
assertThat(res1, `is`(notNullValue()))
orderViewModel.deleteItemFromCart(item1.itemId)
delay(100)
val res2 = orderViewModel.cartItems.getOrAwaitValue().find { it.itemId == item1.itemId }
assertThat(res2, `is`(nullValue()))
}
}
================================================
FILE: app/src/androidTest/java/com/vishalgaur/shoppingapp/viewModels/ProductViewModelTest.kt
================================================
package com.vishalgaur.shoppingapp.viewModels
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vishalgaur.shoppingapp.ServiceLocator
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.FakeAuthRepository
import com.vishalgaur.shoppingapp.data.source.FakeProductsRepository
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
import com.vishalgaur.shoppingapp.getOrAwaitValue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class ProductViewModelTest {
private lateinit var productViewModel: ProductViewModel
private lateinit var productId: String
private lateinit var productsRepository: ProductsRepoInterface
private lateinit var authRepository: AuthRepoInterface
val user = UserData(
"sdjm43892yfh948ehod",
"Vishal",
"+919999988888",
"vishal@somemail.com",
"dh94328hd",
ArrayList(),
ArrayList(),
ArrayList()
)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
productsRepository = FakeProductsRepository()
val sessionManager = ShoppingAppSessionManager(ApplicationProvider.getApplicationContext())
authRepository = FakeAuthRepository(sessionManager)
authRepository.login(user, true)
ServiceLocator.productsRepository = productsRepository
ServiceLocator.authRepository = authRepository
productId = "pro-shoes-wofwopjf-1"
productViewModel = ProductViewModel(productId, ApplicationProvider.getApplicationContext())
}
@After
fun cleanUp() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun toggleLikeProduct_false_true() {
val result1 = productViewModel.isLiked.value
runBlocking {
productViewModel.toggleLikeProduct()
delay(1000)
val result2 = productViewModel.isLiked.getOrAwaitValue()
assertThat(result1, not(result2))
}
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ServiceLocator.kt
================================================
package com.vishalgaur.shoppingapp
import android.content.Context
import androidx.annotation.VisibleForTesting
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.source.ProductDataSource
import com.vishalgaur.shoppingapp.data.source.UserDataSource
import com.vishalgaur.shoppingapp.data.source.local.ProductsLocalDataSource
import com.vishalgaur.shoppingapp.data.source.local.ShoppingAppDatabase
import com.vishalgaur.shoppingapp.data.source.local.UserLocalDataSource
import com.vishalgaur.shoppingapp.data.source.remote.AuthRemoteDataSource
import com.vishalgaur.shoppingapp.data.source.remote.ProductsRemoteDataSource
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepository
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepository
object ServiceLocator {
private var database: ShoppingAppDatabase? = null
private val lock = Any()
@Volatile
var authRepository: AuthRepoInterface? = null
@VisibleForTesting set
@Volatile
var productsRepository: ProductsRepoInterface? = null
@VisibleForTesting set
fun provideAuthRepository(context: Context): AuthRepoInterface {
synchronized(this) {
return authRepository ?: createAuthRepository(context)
}
}
fun provideProductsRepository(context: Context): ProductsRepoInterface {
synchronized(this) {
return productsRepository ?: createProductsRepository(context)
}
}
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
database?.apply {
clearAllTables()
close()
}
database = null
authRepository = null
}
}
private fun createProductsRepository(context: Context): ProductsRepoInterface {
val newRepo =
ProductsRepository(ProductsRemoteDataSource(), createProductsLocalDataSource(context))
productsRepository = newRepo
return newRepo
}
private fun createAuthRepository(context: Context): AuthRepoInterface {
val appSession = ShoppingAppSessionManager(context.applicationContext)
val newRepo =
AuthRepository(createUserLocalDataSource(context), AuthRemoteDataSource(), appSession)
authRepository = newRepo
return newRepo
}
private fun createProductsLocalDataSource(context: Context): ProductDataSource {
val database = database ?: ShoppingAppDatabase.getInstance(context.applicationContext)
return ProductsLocalDataSource(database.productsDao())
}
private fun createUserLocalDataSource(context: Context): UserDataSource {
val database = database ?: ShoppingAppDatabase.getInstance(context.applicationContext)
return UserLocalDataSource(database.userDao())
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ShoppingApplication.kt
================================================
package com.vishalgaur.shoppingapp
import android.app.Application
import com.vishalgaur.shoppingapp.data.source.repository.AuthRepoInterface
import com.vishalgaur.shoppingapp.data.source.repository.ProductsRepoInterface
class ShoppingApplication : Application() {
val authRepository: AuthRepoInterface
get() = ServiceLocator.provideAuthRepository(this)
val productsRepository: ProductsRepoInterface
get() = ServiceLocator.provideProductsRepository(this)
override fun onCreate() {
super.onCreate()
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/Utils.kt
================================================
package com.vishalgaur.shoppingapp
import java.util.*
import java.util.regex.Pattern
import kotlin.math.roundToInt
const val MOB_ERROR_TEXT = "Enter valid mobile number!"
const val EMAIL_ERROR_TEXT = "Enter valid email address!"
const val ERR_INIT = "ERROR"
const val ERR_EMAIL = "_EMAIL"
const val ERR_MOBILE = "_MOBILE"
const val ERR_UPLOAD = "UploadErrorException"
internal fun isEmailValid(email: String): Boolean {
val EMAIL_PATTERN = Pattern.compile(
"\\s*[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
"\\@" +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
"(" +
"\\." +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
")+\\s*"
)
return if (email.isEmpty()) {
false
} else {
EMAIL_PATTERN.matcher(email).matches()
}
}
internal fun isPhoneValid(phone: String): Boolean {
val PHONE_PATTERN = Pattern.compile("^\\s*[6-9]\\d{9}\\s*\$")
return if (phone.isEmpty()) {
false
} else {
PHONE_PATTERN.matcher(phone).matches()
}
}
internal fun isZipCodeValid(zipCode: String): Boolean {
val ZIPCODE_PATTERN = Pattern.compile("^\\s*[1-9]\\d{5}\\s*\$")
return if (zipCode.isEmpty()) {
false
} else {
ZIPCODE_PATTERN.matcher(zipCode).matches()
}
}
internal fun getRandomString(length: Int, uNum: String, endLength: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
fun getStr(l: Int): String = (1..l).map { allowedChars.random() }.joinToString("")
return getStr(length) + uNum + getStr(endLength)
}
internal fun getProductId(ownerId: String, proCategory: String): String {
val uniqueId = UUID.randomUUID().toString()
return "pro-$proCategory-$ownerId-$uniqueId"
}
internal fun getOfferPercentage(costPrice: Double, sellingPrice: Double): Int {
if (costPrice == 0.0 || sellingPrice == 0.0 || costPrice <= sellingPrice)
return 0
val off = ((costPrice - sellingPrice) * 100) / costPrice
return off.roundToInt()
}
internal fun getAddressId(userId: String): String {
val uniqueId = UUID.randomUUID().toString()
return "$userId-$uniqueId"
}
internal fun shouldBypassOTPValidation() : Boolean {
return false
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/Product.kt
================================================
package com.vishalgaur.shoppingapp.data
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "products")
data class Product @JvmOverloads constructor(
@PrimaryKey
var productId: String = "",
var name: String = "",
var owner: String = "",
var description: String = "",
var category: String = "",
var price: Double = 0.0,
var mrp: Double = 0.0,
var availableSizes: List = ArrayList(),
var availableColors: List = ArrayList(),
var images: List = ArrayList(),
var rating: Double = 0.0
) : Parcelable {
fun toHashMap(): HashMap {
return hashMapOf(
"productId" to productId,
"name" to name,
"owner" to owner,
"description" to description,
"category" to category,
"price" to price,
"mrp" to mrp,
"availableSizes" to availableSizes,
"availableColors" to availableColors,
"images" to images,
"rating" to rating
)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/Result.kt
================================================
package com.vishalgaur.shoppingapp.data
import com.vishalgaur.shoppingapp.data.Result.Success
/**
* A generic class that holds a value with its loading status.
* @param
*/
sealed class Result {
data class Success(val data: T) : Result()
data class Error(val exception: Exception) : Result()
object Loading : Result()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
Loading -> "Loading"
}
}
}
/**
* `true` if [Result] is of type [Success] & holds non-null [Success.data].
*/
val Result<*>.succeeded
get() = this is Success && data != null
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/ShoppingAppSessionManager.kt
================================================
package com.vishalgaur.shoppingapp.data
import android.content.Context
import android.content.SharedPreferences
class ShoppingAppSessionManager(context: Context) {
var userSession: SharedPreferences =
context.getSharedPreferences("userSessionData", Context.MODE_PRIVATE)
var editor: SharedPreferences.Editor = userSession.edit()
fun createLoginSession(
id: String,
name: String,
mobile: String,
isRemOn: Boolean,
isSeller: Boolean
) {
editor.putBoolean(IS_LOGIN, true)
editor.putString(KEY_ID, id)
editor.putString(KEY_NAME, name)
editor.putString(KEY_MOBILE, mobile)
editor.putBoolean(KEY_REMEMBER_ME, isRemOn)
editor.putBoolean(KEY_IS_SELLER, isSeller)
editor.commit()
}
fun isUserSeller(): Boolean = userSession.getBoolean(KEY_IS_SELLER, false)
fun isRememberMeOn(): Boolean = userSession.getBoolean(KEY_REMEMBER_ME, false)
fun getPhoneNumber(): String? = userSession.getString(KEY_MOBILE, null)
fun getUserDataFromSession(): HashMap {
return hashMapOf(
KEY_ID to userSession.getString(KEY_ID, null),
KEY_NAME to userSession.getString(KEY_NAME, null),
KEY_MOBILE to userSession.getString(KEY_MOBILE, null)
)
}
fun getUserIdFromSession(): String? = userSession.getString(KEY_ID, null)
fun isLoggedIn(): Boolean = userSession.getBoolean(IS_LOGIN, false)
fun logoutFromSession() {
editor.clear()
editor.commit()
}
companion object {
private const val IS_LOGIN = "isLoggedIn"
private const val KEY_NAME = "userName"
private const val KEY_MOBILE = "userMobile"
private const val KEY_ID = "userId"
private const val KEY_REMEMBER_ME = "isRemOn"
private const val KEY_IS_SELLER = "isSeller"
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/UserData.kt
================================================
package com.vishalgaur.shoppingapp.data
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.vishalgaur.shoppingapp.data.utils.ObjectListTypeConvertor
import com.vishalgaur.shoppingapp.data.utils.OrderStatus
import com.vishalgaur.shoppingapp.data.utils.UserType
import kotlinx.parcelize.Parcelize
import java.util.*
import kotlin.collections.ArrayList
@Parcelize
@Entity(tableName = "users")
data class UserData(
@PrimaryKey
var userId: String = "",
var name: String = "",
var mobile: String = "",
var email: String = "",
var password: String = "",
var likes: List = ArrayList(),
@TypeConverters(ObjectListTypeConvertor::class)
var addresses: List = ArrayList(),
@TypeConverters(ObjectListTypeConvertor::class)
var cart: List = ArrayList(),
@TypeConverters(ObjectListTypeConvertor::class)
var orders: List = ArrayList(),
var userType: String = UserType.CUSTOMER.name
) : Parcelable {
fun toHashMap(): HashMap {
return hashMapOf(
"userId" to userId,
"name" to name,
"email" to email,
"mobile" to mobile,
"password" to password,
"likes" to likes,
"addresses" to addresses.map { it.toHashMap() },
"userType" to userType
)
}
@Parcelize
data class OrderItem(
var orderId: String = "",
var customerId: String = "",
var items: List = ArrayList(),
var itemsPrices: Map = mapOf(),
var deliveryAddress: Address = Address(),
var shippingCharges: Double = 0.0,
var paymentMethod: String = "",
var orderDate: Date = Date(),
var status: String = OrderStatus.CONFIRMED.name
) : Parcelable {
fun toHashMap(): HashMap {
return hashMapOf(
"orderId" to orderId,
"customerId" to customerId,
"items" to items.map { it.toHashMap() },
"itemsPrices" to itemsPrices,
"deliveryAddress" to deliveryAddress.toHashMap(),
"shippingCharges" to shippingCharges,
"paymentMethod" to paymentMethod,
"orderDate" to orderDate,
"status" to status
)
}
}
@Parcelize
data class Address(
var addressId: String = "",
var fName: String = "",
var lName: String = "",
var countryISOCode: String = "",
var streetAddress: String = "",
var streetAddress2: String = "",
var city: String = "",
var state: String = "",
var zipCode: String = "",
var phoneNumber: String = ""
) : Parcelable {
fun toHashMap(): HashMap {
return hashMapOf(
"addressId" to addressId,
"fName" to fName,
"lName" to lName,
"countryISOCode" to countryISOCode,
"streetAddress" to streetAddress,
"streetAddress2" to streetAddress2,
"city" to city,
"state" to state,
"zipCode" to zipCode,
"phoneNumber" to phoneNumber
)
}
}
@Parcelize
data class CartItem(
var itemId: String = "",
var productId: String = "",
var ownerId: String = "",
var quantity: Int = 0,
var color: String?,
var size: Int?
) : Parcelable {
constructor() : this("", "", "", 0, "NA", -1)
fun toHashMap(): HashMap {
val hashMap = hashMapOf()
hashMap["itemId"] = itemId
hashMap["productId"] = productId
hashMap["ownerId"] = ownerId
hashMap["quantity"] = quantity
if (color != null)
hashMap["color"] = color!!
if (size != null)
hashMap["size"] = size!!
return hashMap
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/ProductDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
interface ProductDataSource {
fun observeProducts(): LiveData>?>
suspend fun getAllProducts(): Result>
suspend fun refreshProducts() {}
suspend fun getProductById(productId: String): Result
suspend fun insertProduct(newProduct: Product)
suspend fun updateProduct(proData: Product)
fun observeProductsByOwner(ownerId: String): LiveData>?> {
return MutableLiveData()
}
suspend fun getAllProductsByOwner(ownerId: String): Result> {
return Result.Success(emptyList())
}
suspend fun uploadImage(uri: Uri, fileName: String): Uri? {
return null
}
fun revertUpload(fileName: String) {}
fun deleteImage(imgUrl: String) {}
suspend fun deleteProduct(productId: String)
suspend fun deleteAllProducts() {}
suspend fun insertMultipleProducts(data: List) {}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/UserDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.EmailMobileData
interface UserDataSource {
suspend fun addUser(userData: UserData)
suspend fun getUserById(userId: String): Result
fun updateEmailsAndMobiles(email: String, mobile: String) {}
suspend fun getEmailsAndMobiles(): EmailMobileData? {
return null
}
suspend fun getUserByMobileAndPassword(
mobile: String,
password: String
): MutableList {
return mutableListOf()
}
suspend fun likeProduct(productId: String, userId: String) {}
suspend fun dislikeProduct(productId: String, userId: String) {}
suspend fun insertAddress(newAddress: UserData.Address, userId: String) {}
suspend fun updateAddress(newAddress: UserData.Address, userId: String) {}
suspend fun deleteAddress(addressId: String, userId: String) {}
suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) {}
suspend fun updateCartItem(item: UserData.CartItem, userId: String) {}
suspend fun deleteCartItem(itemId: String, userId: String) {}
suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String) {}
suspend fun setStatusOfOrderByUserId(orderId: String, userId: String, status: String) {}
suspend fun clearUser() {}
suspend fun getUserByMobile(phoneNumber: String): UserData? {
return null
}
suspend fun getOrdersByUserId(userId: String): Result?>
suspend fun getAddressesByUserId(userId: String): Result?>
suspend fun getLikesByUserId(userId: String): Result?>
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/ProductsDao.kt
================================================
package com.vishalgaur.shoppingapp.data.source.local
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.vishalgaur.shoppingapp.data.Product
@Dao
interface ProductsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(product: Product)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertListOfProducts(products: List)
@Query("SELECT * FROM products")
suspend fun getAllProducts(): List
@Query("SELECT * FROM products")
fun observeProducts(): LiveData>
@Query("SELECT * FROM products WHERE owner = :ownerId")
fun observeProductsByOwner(ownerId: String): LiveData>
@Query("SELECT * FROM products WHERE productId = :proId")
suspend fun getProductById(proId: String): Product?
@Query("SELECT * FROM products WHERE owner = :ownerId")
suspend fun getProductsByOwnerId(ownerId: String): List
@Query("DELETE FROM products WHERE productId = :proId")
suspend fun deleteProductById(proId: String): Int
@Query("DELETE FROM products")
suspend fun deleteAllProducts()
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/ProductsLocalDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source.local
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.*
import com.vishalgaur.shoppingapp.data.source.ProductDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ProductsLocalDataSource internal constructor(
private val productsDao: ProductsDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ProductDataSource {
override fun observeProducts(): LiveData>?> {
return try {
Transformations.map(productsDao.observeProducts()) {
Success(it)
}
} catch (e: Exception) {
Transformations.map(MutableLiveData(e)) {
Error(e)
}
}
}
override fun observeProductsByOwner(ownerId: String): LiveData>?> {
return try {
Transformations.map(productsDao.observeProductsByOwner(ownerId)) {
Success(it)
}
} catch (e: Exception) {
Transformations.map(MutableLiveData(e)) {
Error(e)
}
}
}
override suspend fun getAllProducts(): Result> = withContext(ioDispatcher) {
return@withContext try {
Success(productsDao.getAllProducts())
} catch (e: Exception) {
Error(e)
}
}
override suspend fun getAllProductsByOwner(ownerId: String): Result> =
withContext(ioDispatcher) {
return@withContext try {
Success(productsDao.getProductsByOwnerId(ownerId))
} catch (e: Exception) {
Error(e)
}
}
override suspend fun getProductById(productId: String): Result =
withContext(ioDispatcher) {
try {
val product = productsDao.getProductById(productId)
if (product != null) {
return@withContext Success(product)
} else {
return@withContext Error(Exception("Product Not Found!"))
}
} catch (e: Exception) {
return@withContext Error(e)
}
}
override suspend fun insertProduct(newProduct: Product) = withContext(ioDispatcher) {
productsDao.insert(newProduct)
}
override suspend fun updateProduct(proData: Product) = withContext(ioDispatcher) {
productsDao.insert(proData)
}
override suspend fun insertMultipleProducts(data: List) = withContext(ioDispatcher) {
productsDao.insertListOfProducts(data)
}
override suspend fun deleteProduct(productId: String): Unit = withContext(ioDispatcher) {
productsDao.deleteProductById(productId)
}
override suspend fun deleteAllProducts() = withContext(ioDispatcher) {
productsDao.deleteAllProducts()
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/ShoppingAppDatabase.kt
================================================
package com.vishalgaur.shoppingapp.data.source.local
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.DateTypeConvertors
import com.vishalgaur.shoppingapp.data.utils.ListTypeConverter
import com.vishalgaur.shoppingapp.data.utils.ObjectListTypeConvertor
@Database(entities = [UserData::class, Product::class], version = 2)
@TypeConverters(ListTypeConverter::class, ObjectListTypeConvertor::class, DateTypeConvertors::class)
abstract class ShoppingAppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun productsDao(): ProductsDao
companion object {
@Volatile
private var INSTANCE: ShoppingAppDatabase? = null
fun getInstance(context: Context): ShoppingAppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
ShoppingAppDatabase::class.java, "ShoppingAppDb"
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/UserDao.kt
================================================
package com.vishalgaur.shoppingapp.data.source.local
import androidx.room.*
import com.vishalgaur.shoppingapp.data.UserData
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(uData: UserData)
@Query("SELECT * FROM users WHERE userId = :userId")
suspend fun getById(userId: String): UserData?
@Query("SELECT * FROM users WHERE mobile = :mobile")
suspend fun getByMobile(mobile: String): UserData?
@Update(entity = UserData::class)
suspend fun updateUser(obj: UserData)
@Query("DELETE FROM users")
suspend fun clear()
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/local/UserLocalDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source.local
import android.util.Log
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.*
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.UserDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class UserLocalDataSource internal constructor(
private val userDao: UserDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserDataSource {
override suspend fun addUser(userData: UserData) {
withContext(ioDispatcher) {
userDao.clear()
userDao.insert(userData)
}
}
override suspend fun getUserById(userId: String): Result =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
return@withContext Success(uData)
} else {
return@withContext Error(Exception("User Not Found!"))
}
} catch (e: Exception) {
return@withContext Error(e)
}
}
override suspend fun getUserByMobile(phoneNumber: String): UserData? =
withContext(ioDispatcher) {
try {
val uData = userDao.getByMobile(phoneNumber)
if (uData != null) {
return@withContext uData
} else {
return@withContext null
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onGetUser: Error Occurred, $e")
return@withContext null
}
}
override suspend fun getOrdersByUserId(userId: String): Result?> =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val ordersList = uData.orders
return@withContext Success(ordersList)
} else {
return@withContext Error(Exception("User Not Found"))
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onGetOrders: Error Occurred, ${e.message}")
return@withContext Error(e)
}
}
override suspend fun getAddressesByUserId(userId: String): Result?> =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val addressList = uData.addresses
return@withContext Success(addressList)
} else {
return@withContext Error(Exception("User Not Found"))
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onGetAddress: Error Occurred, ${e.message}")
return@withContext Error(e)
}
}
override suspend fun getLikesByUserId(userId: String): Result?> =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val likesList = uData.likes
return@withContext Success(likesList)
} else {
return@withContext Error(Exception("User Not Found"))
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onGetLikes: Error Occurred, ${e.message}")
return@withContext Error(e)
}
}
override suspend fun dislikeProduct(productId: String, userId: String) =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val likesList = uData.likes.toMutableList()
likesList.remove(productId)
uData.likes = likesList
userDao.updateUser(uData)
} else {
throw Exception("User Not Found")
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onGetLikes: Error Occurred, ${e.message}")
throw e
}
}
override suspend fun likeProduct(productId: String, userId: String) =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val likesList = uData.likes.toMutableList()
likesList.add(productId)
uData.likes = likesList
userDao.updateUser(uData)
} else {
throw Exception("User Not Found")
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onGetLikes: Error Occurred, ${e.message}")
throw e
}
}
override suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val cartItems = uData.cart.toMutableList()
cartItems.add(newItem)
uData.cart = cartItems
userDao.updateUser(uData)
} else {
throw Exception("User Not Found")
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}")
throw e
}
}
override suspend fun updateCartItem(item: UserData.CartItem, userId: String) =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val cartItems = uData.cart.toMutableList()
val pos = cartItems.indexOfFirst { it.itemId == item.itemId }
if (pos >= 0) {
cartItems[pos] = item
}
uData.cart = cartItems
userDao.updateUser(uData)
} else {
throw Exception("User Not Found")
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}")
throw e
}
}
override suspend fun deleteCartItem(itemId: String, userId: String) =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val cartItems = uData.cart.toMutableList()
val pos = cartItems.indexOfFirst { it.itemId == itemId }
if (pos >= 0) {
cartItems.removeAt(pos)
}
uData.cart = cartItems
userDao.updateUser(uData)
} else {
throw Exception("User Not Found")
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}")
throw e
}
}
override suspend fun setStatusOfOrderByUserId(orderId: String, userId: String, status: String) =
withContext(ioDispatcher) {
try {
val uData = userDao.getById(userId)
if (uData != null) {
val orders = uData.orders.toMutableList()
val pos = orders.indexOfFirst { it.orderId == orderId }
if (pos >= 0) {
orders[pos].status = status
val custId = orders[pos].customerId
val custData = userDao.getById(custId)
if (custData != null) {
val orderList = custData.orders.toMutableList()
val idx = orderList.indexOfFirst { it.orderId == orderId }
if (idx >= 0) {
orderList[idx].status = status
}
custData.orders = orderList
userDao.updateUser(custData)
}
}
uData.orders = orders
userDao.updateUser(uData)
} else {
throw Exception("User Not Found")
}
} catch (e: Exception) {
Log.d("UserLocalSource", "onInsertCartItem: Error Occurred, ${e.message}")
throw e
}
}
override suspend fun clearUser() {
withContext(ioDispatcher) {
userDao.clear()
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/remote/AuthRemoteDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source.remote
import android.util.Log
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.UserDataSource
import com.vishalgaur.shoppingapp.data.utils.EmailMobileData
import com.vishalgaur.shoppingapp.data.utils.OrderStatus
import kotlinx.coroutines.tasks.await
class AuthRemoteDataSource : UserDataSource {
private val firebaseDb: FirebaseFirestore = Firebase.firestore
private fun usersCollectionRef() = firebaseDb.collection(USERS_COLLECTION)
private fun allEmailsMobilesRef() =
firebaseDb.collection(USERS_COLLECTION).document(EMAIL_MOBILE_DOC)
override suspend fun getUserById(userId: String): Result {
val resRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
return if (!resRef.isEmpty) {
Success(resRef.toObjects(UserData::class.java)[0])
} else {
Error(Exception("User Not Found!"))
}
}
override suspend fun addUser(userData: UserData) {
usersCollectionRef().add(userData.toHashMap())
.addOnSuccessListener {
Log.d(TAG, "Doc added")
}
.addOnFailureListener { e ->
Log.d(TAG, "firestore error occurred: $e")
}
}
override suspend fun getUserByMobile(phoneNumber: String): UserData =
usersCollectionRef().whereEqualTo(USERS_MOBILE_FIELD, phoneNumber).get().await()
.toObjects(UserData::class.java)[0]
override suspend fun getOrdersByUserId(userId: String): Result?> {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
return if (!userRef.isEmpty) {
val userData = userRef.documents[0].toObject(UserData::class.java)
Success(userData!!.orders)
} else {
Error(Exception("User Not Found!"))
}
}
override suspend fun getAddressesByUserId(userId: String): Result?> {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
return if (!userRef.isEmpty) {
val userData = userRef.documents[0].toObject(UserData::class.java)
Success(userData!!.addresses)
} else {
Error(Exception("User Not Found!"))
}
}
override suspend fun getLikesByUserId(userId: String): Result?> {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
return if (!userRef.isEmpty) {
val userData = userRef.documents[0].toObject(UserData::class.java)
Success(userData!!.likes)
} else {
Error(Exception("User Not Found!"))
}
}
override suspend fun getUserByMobileAndPassword(
mobile: String,
password: String
): MutableList =
usersCollectionRef().whereEqualTo(USERS_MOBILE_FIELD, mobile)
.whereEqualTo(USERS_PWD_FIELD, password).get().await().toObjects(UserData::class.java)
override suspend fun likeProduct(productId: String, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
usersCollectionRef().document(docId)
.update(USERS_LIKES_FIELD, FieldValue.arrayUnion(productId))
}
}
override suspend fun dislikeProduct(productId: String, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
usersCollectionRef().document(docId)
.update(USERS_LIKES_FIELD, FieldValue.arrayRemove(productId))
}
}
override suspend fun insertAddress(newAddress: UserData.Address, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
usersCollectionRef().document(docId)
.update(USERS_ADDRESSES_FIELD, FieldValue.arrayUnion(newAddress.toHashMap()))
}
}
override suspend fun updateAddress(newAddress: UserData.Address, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
val oldAddressList =
userRef.documents[0].toObject(UserData::class.java)?.addresses?.toMutableList()
val idx = oldAddressList?.indexOfFirst { it.addressId == newAddress.addressId } ?: -1
if (idx != -1) {
oldAddressList?.set(idx, newAddress)
}
usersCollectionRef().document(docId)
.update(USERS_ADDRESSES_FIELD, oldAddressList?.map { it.toHashMap() })
}
}
override suspend fun deleteAddress(addressId: String, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
val oldAddressList =
userRef.documents[0].toObject(UserData::class.java)?.addresses?.toMutableList()
val idx = oldAddressList?.indexOfFirst { it.addressId == addressId } ?: -1
if (idx != -1) {
oldAddressList?.removeAt(idx)
}
usersCollectionRef().document(docId)
.update(USERS_ADDRESSES_FIELD, oldAddressList?.map { it.toHashMap() })
}
}
override suspend fun insertCartItem(newItem: UserData.CartItem, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
usersCollectionRef().document(docId)
.update(USERS_CART_FIELD, FieldValue.arrayUnion(newItem.toHashMap()))
}
}
override suspend fun updateCartItem(item: UserData.CartItem, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
val oldCart =
userRef.documents[0].toObject(UserData::class.java)?.cart?.toMutableList()
val idx = oldCart?.indexOfFirst { it.itemId == item.itemId } ?: -1
if (idx != -1) {
oldCart?.set(idx, item)
}
usersCollectionRef().document(docId)
.update(USERS_CART_FIELD, oldCart?.map { it.toHashMap() })
}
}
override suspend fun deleteCartItem(itemId: String, userId: String) {
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
val oldCart =
userRef.documents[0].toObject(UserData::class.java)?.cart?.toMutableList()
val idx = oldCart?.indexOfFirst { it.itemId == itemId } ?: -1
if (idx != -1) {
oldCart?.removeAt(idx)
}
usersCollectionRef().document(docId)
.update(USERS_CART_FIELD, oldCart?.map { it.toHashMap() })
}
}
override suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String) {
// add order to customer and
// specific items to their owners
// empty customers cart
val ownerProducts: MutableMap> = mutableMapOf()
for (item in newOrder.items) {
if (!ownerProducts.containsKey(item.ownerId)) {
ownerProducts[item.ownerId] = mutableListOf()
}
ownerProducts[item.ownerId]?.add(item)
}
ownerProducts.forEach { (ownerId, items) ->
run {
val itemPrices = mutableMapOf()
items.forEach { item ->
itemPrices[item.itemId] = newOrder.itemsPrices[item.itemId] ?: 0.0
}
val ownerOrder = UserData.OrderItem(
newOrder.orderId,
userId,
items,
itemPrices,
newOrder.deliveryAddress,
newOrder.shippingCharges,
newOrder.paymentMethod,
newOrder.orderDate,
OrderStatus.CONFIRMED.name
)
val ownerRef =
usersCollectionRef().whereEqualTo(USERS_ID_FIELD, ownerId).get().await()
if (!ownerRef.isEmpty) {
val docId = ownerRef.documents[0].id
usersCollectionRef().document(docId)
.update(USERS_ORDERS_FIELD, FieldValue.arrayUnion(ownerOrder.toHashMap()))
}
}
}
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
usersCollectionRef().document(docId)
.update(USERS_ORDERS_FIELD, FieldValue.arrayUnion(newOrder.toHashMap()))
usersCollectionRef().document(docId)
.update(USERS_CART_FIELD, ArrayList())
}
}
override suspend fun setStatusOfOrderByUserId(orderId: String, userId: String, status: String) {
// update on customer and owner
val userRef = usersCollectionRef().whereEqualTo(USERS_ID_FIELD, userId).get().await()
if (!userRef.isEmpty) {
val docId = userRef.documents[0].id
val ordersList =
userRef.documents[0].toObject(UserData::class.java)?.orders?.toMutableList()
val idx = ordersList?.indexOfFirst { it.orderId == orderId } ?: -1
if (idx != -1) {
val orderData = ordersList?.get(idx)
if (orderData != null) {
usersCollectionRef().document(docId)
.update(USERS_ORDERS_FIELD, FieldValue.arrayRemove(orderData.toHashMap()))
orderData.status = status
usersCollectionRef().document(docId)
.update(USERS_ORDERS_FIELD, FieldValue.arrayUnion(orderData.toHashMap()))
// updating customer status
val custRef =
usersCollectionRef().whereEqualTo(USERS_ID_FIELD, orderData.customerId)
.get().await()
if (!custRef.isEmpty) {
val did = custRef.documents[0].id
val orders =
custRef.documents[0].toObject(UserData::class.java)?.orders?.toMutableList()
val pos = orders?.indexOfFirst { it.orderId == orderId } ?: -1
if (pos != -1) {
val order = orders?.get(pos)
if (order != null) {
usersCollectionRef().document(did).update(
USERS_ORDERS_FIELD,
FieldValue.arrayRemove(order.toHashMap())
)
order.status = status
usersCollectionRef().document(did).update(
USERS_ORDERS_FIELD,
FieldValue.arrayUnion(order.toHashMap())
)
}
}
}
}
}
}
}
override fun updateEmailsAndMobiles(email: String, mobile: String) {
allEmailsMobilesRef().update(EMAIL_MOBILE_EMAIL_FIELD, FieldValue.arrayUnion(email))
allEmailsMobilesRef().update(EMAIL_MOBILE_MOB_FIELD, FieldValue.arrayUnion(mobile))
}
override suspend fun getEmailsAndMobiles() = allEmailsMobilesRef().get().await().toObject(
EmailMobileData::class.java
)
companion object {
private const val USERS_COLLECTION = "users"
private const val USERS_ID_FIELD = "userId"
private const val USERS_ADDRESSES_FIELD = "addresses"
private const val USERS_LIKES_FIELD = "likes"
private const val USERS_CART_FIELD = "cart"
private const val USERS_ORDERS_FIELD = "orders"
private const val USERS_MOBILE_FIELD = "mobile"
private const val USERS_PWD_FIELD = "password"
private const val EMAIL_MOBILE_DOC = "emailAndMobiles"
private const val EMAIL_MOBILE_EMAIL_FIELD = "emails"
private const val EMAIL_MOBILE_MOB_FIELD = "mobiles"
private const val TAG = "AuthRemoteDataSource"
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/remote/ProductsRemoteDataSource.kt
================================================
package com.vishalgaur.shoppingapp.data.source.remote
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.ktx.storage
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
import com.vishalgaur.shoppingapp.data.source.ProductDataSource
import kotlinx.coroutines.tasks.await
class ProductsRemoteDataSource : ProductDataSource {
private val firebaseDb: FirebaseFirestore = Firebase.firestore
private val firebaseStorage: FirebaseStorage = Firebase.storage
private val observableProducts = MutableLiveData>?>()
private fun storageRef() = firebaseStorage.reference
private fun productsCollectionRef() = firebaseDb.collection(PRODUCT_COLLECTION)
override suspend fun refreshProducts() {
observableProducts.value = getAllProducts()
}
override fun observeProducts(): LiveData>?> {
return observableProducts
}
override suspend fun getAllProducts(): Result> {
val resRef = productsCollectionRef().get().await()
return if (!resRef.isEmpty) {
Success(resRef.toObjects(Product::class.java))
} else {
Error(Exception("Error getting Products!"))
}
}
override suspend fun insertProduct(newProduct: Product) {
productsCollectionRef().add(newProduct.toHashMap()).await()
}
override suspend fun updateProduct(proData: Product) {
val resRef =
productsCollectionRef().whereEqualTo(PRODUCT_ID_FIELD, proData.productId).get().await()
if (!resRef.isEmpty) {
val docId = resRef.documents[0].id
productsCollectionRef().document(docId).set(proData.toHashMap()).await()
} else {
Log.d(TAG, "onUpdateProduct: product with id: $proData.productId not found!")
}
}
override suspend fun getProductById(productId: String): Result {
val resRef = productsCollectionRef().whereEqualTo(PRODUCT_ID_FIELD, productId).get().await()
return if (!resRef.isEmpty) {
Success(resRef.toObjects(Product::class.java)[0])
} else {
Error(Exception("Product with id: $productId Not Found!"))
}
}
override suspend fun deleteProduct(productId: String) {
Log.d(TAG, "onDeleteProduct: delete product with Id: $productId initiated")
val resRef = productsCollectionRef().whereEqualTo(PRODUCT_ID_FIELD, productId).get().await()
if (!resRef.isEmpty) {
val product = resRef.documents[0].toObject(Product::class.java)
val imgUrls = product?.images
//deleting images first
imgUrls?.forEach { imgUrl ->
deleteImage(imgUrl)
}
//deleting doc containing product
val docId = resRef.documents[0].id
productsCollectionRef().document(docId).delete().addOnSuccessListener {
Log.d(TAG, "onDelete: DocumentSnapshot successfully deleted!")
}.addOnFailureListener { e ->
Log.w(TAG, "onDelete: Error deleting document", e)
}
} else {
Log.d(TAG, "onDeleteProduct: product with id: $productId not found!")
}
}
override suspend fun uploadImage(uri: Uri, fileName: String): Uri? {
val imgRef = storageRef().child("$SHOES_STORAGE_PATH/$fileName")
val uploadTask = imgRef.putFile(uri)
val uriRef = uploadTask.continueWithTask { task ->
if (!task.isSuccessful) {
task.exception?.let { throw it }
}
imgRef.downloadUrl
}
return uriRef.await()
}
override fun deleteImage(imgUrl: String) {
val ref = firebaseStorage.getReferenceFromUrl(imgUrl)
ref.delete().addOnSuccessListener {
Log.d(TAG, "onDelete: image deleted successfully!")
}.addOnFailureListener { e ->
Log.d(TAG, "onDelete: Error deleting image, error: $e")
}
}
override fun revertUpload(fileName: String) {
val imgRef = storageRef().child("${SHOES_STORAGE_PATH}/$fileName")
imgRef.delete().addOnSuccessListener {
Log.d(TAG, "onRevert: File with name: $fileName deleted successfully!")
}.addOnFailureListener { e ->
Log.d(TAG, "onRevert: Error deleting file with name = $fileName, error: $e")
}
}
companion object {
private const val PRODUCT_COLLECTION = "products"
private const val PRODUCT_ID_FIELD = "productId"
private const val SHOES_STORAGE_PATH = "Shoes"
private const val TAG = "ProductsRemoteSource"
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/AuthRepoInterface.kt
================================================
package com.vishalgaur.shoppingapp.data.source.repository
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.PhoneAuthCredential
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.SignUpErrors
interface AuthRepoInterface {
suspend fun refreshData()
suspend fun signUp(userData: UserData)
fun login(userData: UserData, rememberMe: Boolean)
suspend fun checkEmailAndMobile(email: String, mobile: String, context: Context): SignUpErrors?
suspend fun checkLogin(mobile: String, password: String): UserData?
suspend fun signOut()
suspend fun hardRefreshUserData()
suspend fun insertProductToLikes(productId: String, userId: String): Result
suspend fun removeProductFromLikes(productId: String, userId: String): Result
suspend fun insertAddress(newAddress: UserData.Address, userId: String): Result
suspend fun updateAddress(newAddress: UserData.Address, userId: String): Result
suspend fun deleteAddressById(addressId: String, userId: String): Result
suspend fun insertCartItemByUserId(cartItem: UserData.CartItem, userId: String): Result
suspend fun updateCartItemByUserId(cartItem: UserData.CartItem, userId: String): Result
suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result
suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String): Result
suspend fun setStatusOfOrder(orderId: String, userId: String, status: String): Result
suspend fun getOrdersByUserId(userId: String): Result?>
suspend fun getAddressesByUserId(userId: String): Result?>
suspend fun getLikesByUserId(userId: String): Result?>
suspend fun getUserData(userId: String): Result
fun getFirebaseAuth(): FirebaseAuth
fun signInWithPhoneAuthCredential(
credential: PhoneAuthCredential,
isUserLoggedIn: MutableLiveData,
context: Context
)
fun isRememberMeOn(): Boolean
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/AuthRepository.kt
================================================
package com.vishalgaur.shoppingapp.data.source.repository
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.Error
import com.vishalgaur.shoppingapp.data.Result.Success
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.source.UserDataSource
import com.vishalgaur.shoppingapp.data.utils.SignUpErrors
import com.vishalgaur.shoppingapp.data.utils.UserType
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
class AuthRepository(
private val userLocalDataSource: UserDataSource,
private val authRemoteDataSource: UserDataSource,
private var sessionManager: ShoppingAppSessionManager
) : AuthRepoInterface {
private var firebaseAuth: FirebaseAuth = Firebase.auth
companion object {
private const val TAG = "AuthRepository"
}
override fun getFirebaseAuth() = firebaseAuth
override fun isRememberMeOn() = sessionManager.isRememberMeOn()
override suspend fun refreshData() {
Log.d(TAG, "refreshing userdata")
if (sessionManager.isLoggedIn()) {
updateUserInLocalSource(sessionManager.getPhoneNumber())
} else {
sessionManager.logoutFromSession()
deleteUserFromLocalSource()
}
}
override suspend fun signUp(userData: UserData) {
val isSeller = userData.userType == UserType.SELLER.name
sessionManager.createLoginSession(
userData.userId,
userData.name,
userData.mobile,
false,
isSeller
)
Log.d(TAG, "on SignUp: Updating user in Local Source")
userLocalDataSource.addUser(userData)
Log.d(TAG, "on SignUp: Updating userdata on Remote Source")
authRemoteDataSource.addUser(userData)
authRemoteDataSource.updateEmailsAndMobiles(userData.email, userData.mobile)
}
override fun login(userData: UserData, rememberMe: Boolean) {
val isSeller = userData.userType == UserType.SELLER.name
sessionManager.createLoginSession(
userData.userId,
userData.name,
userData.mobile,
rememberMe,
isSeller
)
}
override suspend fun checkEmailAndMobile(
email: String,
mobile: String,
context: Context
): SignUpErrors? {
Log.d(TAG, "on SignUp: Checking email and mobile")
var sErr: SignUpErrors? = null
try {
val queryResult = authRemoteDataSource.getEmailsAndMobiles()
if (queryResult != null) {
val mob = queryResult.mobiles.contains(mobile)
val em = queryResult.emails.contains(email)
if (!mob && !em) {
sErr = SignUpErrors.NONE
} else {
sErr = SignUpErrors.SERR
when {
!mob && em -> makeErrToast("Email is already registered!", context)
mob && !em -> makeErrToast("Mobile is already registered!", context)
mob && em -> makeErrToast(
"Email and mobile is already registered!",
context
)
}
}
}
} catch (e: Exception) {
makeErrToast("Some Error Occurred", context)
}
return sErr
}
override suspend fun checkLogin(mobile: String, password: String): UserData? {
Log.d(TAG, "on Login: checking mobile and password")
var queryResult = mutableListOf()
try {
queryResult = authRemoteDataSource.getUserByMobileAndPassword(mobile, password)
} catch (e: Exception) {
// No Handling
}
return if (queryResult.size > 0) {
queryResult[0]
} else {
null
}
}
override fun signInWithPhoneAuthCredential(
credential: PhoneAuthCredential,
isUserLoggedIn: MutableLiveData, context: Context
) {
try {
firebaseAuth.signInWithCredential(credential)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "signInWithCredential:success")
val user = task.result?.user
if (user != null) {
isUserLoggedIn.postValue(true)
}
} else {
Log.w(TAG, "signInWithCredential:failure", task.exception)
if (task.exception is FirebaseAuthInvalidCredentialsException) {
Log.d(TAG, "createUserWithMobile:failure", task.exception)
isUserLoggedIn.postValue(false)
makeErrToast("Wrong OTP!", context)
}
}
}.addOnFailureListener {
Log.d(TAG, "createUserWithMobile:failure", it)
isUserLoggedIn.postValue(false)
makeErrToast("Invalid Request!", context)
}
} catch (e: Exception) {
makeErrToast("Some Error Occurred", context)
}
}
override suspend fun signOut() {
sessionManager.logoutFromSession()
firebaseAuth.signOut()
userLocalDataSource.clearUser()
}
private fun makeErrToast(text: String, context: Context) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
private suspend fun deleteUserFromLocalSource() {
userLocalDataSource.clearUser()
}
private suspend fun updateUserInLocalSource(phoneNumber: String?) {
coroutineScope {
launch {
if (phoneNumber != null) {
val getUser = userLocalDataSource.getUserByMobile(phoneNumber)
if (getUser == null) {
userLocalDataSource.clearUser()
val uData = authRemoteDataSource.getUserByMobile(phoneNumber)
if (uData != null) {
userLocalDataSource.addUser(uData)
}
}
}
}
}
}
override suspend fun hardRefreshUserData() {
userLocalDataSource.clearUser()
val mobile = sessionManager.getPhoneNumber()
if (mobile != null) {
val uData = authRemoteDataSource.getUserByMobile(mobile)
if (uData != null) {
userLocalDataSource.addUser(uData)
}
}
}
override suspend fun insertProductToLikes(productId: String, userId: String): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onLikeProduct: adding product to remote source")
authRemoteDataSource.likeProduct(productId, userId)
}
val localRes = async {
Log.d(TAG, "onLikeProduct: updating product to local source")
userLocalDataSource.likeProduct(productId, userId)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun removeProductFromLikes(
productId: String,
userId: String
): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onDislikeProduct: deleting product from remote source")
authRemoteDataSource.dislikeProduct(productId, userId)
}
val localRes = async {
Log.d(TAG, "onDislikeProduct: updating product to local source")
userLocalDataSource.dislikeProduct(productId, userId)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun insertAddress(
newAddress: UserData.Address,
userId: String
): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onInsertAddress: adding address to remote source")
authRemoteDataSource.insertAddress(newAddress, userId)
}
val localRes = async {
Log.d(TAG, "onInsertAddress: updating address to local source")
val userRes = authRemoteDataSource.getUserById(userId)
if (userRes is Success) {
userLocalDataSource.clearUser()
userLocalDataSource.addUser(userRes.data!!)
} else if (userRes is Error) {
throw userRes.exception
}
}
try {
remoteRes.await()
localRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun updateAddress(
newAddress: UserData.Address,
userId: String
): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onUpdateAddress: updating address on remote source")
authRemoteDataSource.updateAddress(newAddress, userId)
}
val localRes = async {
Log.d(TAG, "onUpdateAddress: updating address on local source")
val userRes =
authRemoteDataSource.getUserById(userId)
if (userRes is Success) {
userLocalDataSource.clearUser()
userLocalDataSource.addUser(userRes.data!!)
} else if (userRes is Error) {
throw userRes.exception
}
}
try {
remoteRes.await()
localRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun deleteAddressById(addressId: String, userId: String): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onDelete: deleting address from remote source")
authRemoteDataSource.deleteAddress(addressId, userId)
}
val localRes = async {
Log.d(TAG, "onDelete: deleting address from local source")
val userRes =
authRemoteDataSource.getUserById(userId)
if (userRes is Success) {
userLocalDataSource.clearUser()
userLocalDataSource.addUser(userRes.data!!)
} else if (userRes is Error) {
throw userRes.exception
}
}
try {
remoteRes.await()
localRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun insertCartItemByUserId(
cartItem: UserData.CartItem,
userId: String
): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onInsertCartItem: adding item to remote source")
authRemoteDataSource.insertCartItem(cartItem, userId)
}
val localRes = async {
Log.d(TAG, "onInsertCartItem: updating item to local source")
userLocalDataSource.insertCartItem(cartItem, userId)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun updateCartItemByUserId(
cartItem: UserData.CartItem,
userId: String
): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onUpdateCartItem: updating cart item on remote source")
authRemoteDataSource.updateCartItem(cartItem, userId)
}
val localRes = async {
Log.d(TAG, "onUpdateCartItem: updating cart item on local source")
userLocalDataSource.updateCartItem(cartItem, userId)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun deleteCartItemByUserId(itemId: String, userId: String): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onDelete: deleting cart item from remote source")
authRemoteDataSource.deleteCartItem(itemId, userId)
}
val localRes = async {
Log.d(TAG, "onDelete: deleting cart item from local source")
userLocalDataSource.deleteCartItem(itemId, userId)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun placeOrder(newOrder: UserData.OrderItem, userId: String): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onPlaceOrder: adding item to remote source")
authRemoteDataSource.placeOrder(newOrder, userId)
}
val localRes = async {
Log.d(TAG, "onPlaceOrder: adding item to local source")
val userRes = authRemoteDataSource.getUserById(userId)
if (userRes is Success) {
userLocalDataSource.clearUser()
userLocalDataSource.addUser(userRes.data!!)
} else if (userRes is Error) {
throw userRes.exception
}
}
try {
remoteRes.await()
localRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun setStatusOfOrder(
orderId: String,
userId: String,
status: String
): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onSetStatus: updating status on remote source")
authRemoteDataSource.setStatusOfOrderByUserId(orderId, userId, status)
}
val localRes = async {
Log.d(TAG, "onSetStatus: updating status on local source")
userLocalDataSource.setStatusOfOrderByUserId(orderId, userId, status)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun getOrdersByUserId(userId: String): Result?> {
return userLocalDataSource.getOrdersByUserId(userId)
}
override suspend fun getAddressesByUserId(userId: String): Result?> {
return userLocalDataSource.getAddressesByUserId(userId)
}
override suspend fun getLikesByUserId(userId: String): Result?> {
return userLocalDataSource.getLikesByUserId(userId)
}
override suspend fun getUserData(userId: String): Result {
return userLocalDataSource.getUserById(userId)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/ProductsRepoInterface.kt
================================================
package com.vishalgaur.shoppingapp.data.source.repository
import android.net.Uri
import androidx.lifecycle.LiveData
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
interface ProductsRepoInterface {
suspend fun refreshProducts(): StoreDataStatus?
fun observeProducts(): LiveData>?>
fun observeProductsByOwner(ownerId: String): LiveData>?>
suspend fun getAllProductsByOwner(ownerId: String): Result>
suspend fun getProductById(productId: String, forceUpdate: Boolean = false): Result
suspend fun insertProduct(newProduct: Product): Result
suspend fun insertImages(imgList: List): List
suspend fun updateProduct(product: Product): Result
suspend fun updateImages(newList: List, oldList: List): List
suspend fun deleteProductById(productId: String): Result
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/source/repository/ProductsRepository.kt
================================================
package com.vishalgaur.shoppingapp.data.source.repository
import android.net.Uri
import android.util.Log
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import com.vishalgaur.shoppingapp.ERR_UPLOAD
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.Result
import com.vishalgaur.shoppingapp.data.Result.*
import com.vishalgaur.shoppingapp.data.source.ProductDataSource
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope
import java.util.*
class ProductsRepository(
private val productsRemoteSource: ProductDataSource,
private val productsLocalSource: ProductDataSource
) : ProductsRepoInterface {
companion object {
private const val TAG = "ProductsRepository"
}
override suspend fun refreshProducts(): StoreDataStatus? {
Log.d(TAG, "Updating Products in Room")
return updateProductsFromRemoteSource()
}
override fun observeProducts(): LiveData>?> {
return productsLocalSource.observeProducts()
}
override fun observeProductsByOwner(ownerId: String): LiveData>?> {
return productsLocalSource.observeProductsByOwner(ownerId)
}
override suspend fun getAllProductsByOwner(ownerId: String): Result> {
return productsLocalSource.getAllProductsByOwner(ownerId)
}
override suspend fun getProductById(productId: String, forceUpdate: Boolean): Result {
if (forceUpdate) {
updateProductFromRemoteSource(productId)
}
return productsLocalSource.getProductById(productId)
}
override suspend fun insertProduct(newProduct: Product): Result {
return supervisorScope {
val localRes = async {
Log.d(TAG, "onInsertProduct: adding product to local source")
productsLocalSource.insertProduct(newProduct)
}
val remoteRes = async {
Log.d(TAG, "onInsertProduct: adding product to remote source")
productsRemoteSource.insertProduct(newProduct)
}
try {
localRes.await()
remoteRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun insertImages(imgList: List): List {
var urlList = mutableListOf()
imgList.forEach label@{ uri ->
val uniId = UUID.randomUUID().toString()
val fileName = uniId + uri.lastPathSegment?.split("/")?.last()
try {
val downloadUrl = productsRemoteSource.uploadImage(uri, fileName)
urlList.add(downloadUrl.toString())
} catch (e: Exception) {
productsRemoteSource.revertUpload(fileName)
Log.d(TAG, "exception: message = $e")
urlList = mutableListOf()
urlList.add(ERR_UPLOAD)
return@label
}
}
return urlList
}
override suspend fun updateProduct(product: Product): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onUpdate: updating product in remote source")
productsRemoteSource.updateProduct(product)
}
val localRes = async {
Log.d(TAG, "onUpdate: updating product in local source")
productsLocalSource.insertProduct(product)
}
try {
remoteRes.await()
localRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
override suspend fun updateImages(newList: List, oldList: List): List {
var urlList = mutableListOf()
newList.forEach label@{ uri ->
if (!oldList.contains(uri.toString())) {
val uniId = UUID.randomUUID().toString()
val fileName = uniId + uri.lastPathSegment?.split("/")?.last()
try {
val downloadUrl = productsRemoteSource.uploadImage(uri, fileName)
urlList.add(downloadUrl.toString())
} catch (e: Exception) {
productsRemoteSource.revertUpload(fileName)
Log.d(TAG, "exception: message = $e")
urlList = mutableListOf()
urlList.add(ERR_UPLOAD)
return@label
}
} else {
urlList.add(uri.toString())
}
}
oldList.forEach { imgUrl ->
if (!newList.contains(imgUrl.toUri())) {
productsRemoteSource.deleteImage(imgUrl)
}
}
return urlList
}
override suspend fun deleteProductById(productId: String): Result {
return supervisorScope {
val remoteRes = async {
Log.d(TAG, "onDelete: deleting product from remote source")
productsRemoteSource.deleteProduct(productId)
}
val localRes = async {
Log.d(TAG, "onDelete: deleting product from local source")
productsLocalSource.deleteProduct(productId)
}
try {
remoteRes.await()
localRes.await()
Success(true)
} catch (e: Exception) {
Error(e)
}
}
}
private suspend fun updateProductsFromRemoteSource(): StoreDataStatus? {
var res: StoreDataStatus? = null
try {
val remoteProducts = productsRemoteSource.getAllProducts()
if (remoteProducts is Success) {
Log.d(TAG, "pro list = ${remoteProducts.data}")
productsLocalSource.deleteAllProducts()
productsLocalSource.insertMultipleProducts(remoteProducts.data)
res = StoreDataStatus.DONE
} else {
res = StoreDataStatus.ERROR
if (remoteProducts is Error)
throw remoteProducts.exception
}
} catch (e: Exception) {
Log.d(TAG, "onUpdateProductsFromRemoteSource: Exception occurred, ${e.message}")
}
return res
}
private suspend fun updateProductFromRemoteSource(productId: String): StoreDataStatus? {
var res: StoreDataStatus? = null
try {
val remoteProduct = productsRemoteSource.getProductById(productId)
if (remoteProduct is Success) {
productsLocalSource.insertProduct(remoteProduct.data)
res = StoreDataStatus.DONE
} else {
res = StoreDataStatus.ERROR
if (remoteProduct is Error)
throw remoteProduct.exception
}
} catch (e: Exception) {
Log.d(TAG, "onUpdateProductFromRemoteSource: Exception occurred, ${e.message}")
}
return res
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/DateTypeConvertors.kt
================================================
package com.vishalgaur.shoppingapp.data.utils
import androidx.room.TypeConverter
import java.util.*
class DateTypeConvertors {
@TypeConverter
fun toDate(dateLong: Long?): Date? {
return dateLong?.let { Date(it) }
}
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/EmailMobileData.kt
================================================
package com.vishalgaur.shoppingapp.data.utils
data class EmailMobileData(
val emails: ArrayList = ArrayList(),
val mobiles: ArrayList = ArrayList()
)
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/ListTypeConverter.kt
================================================
package com.vishalgaur.shoppingapp.data.utils
import androidx.room.TypeConverter
class ListTypeConverter {
@TypeConverter
fun fromStringToStringList(value: String): List {
return value.split(",").map { it }
}
@TypeConverter
fun fromStringListToString(value: List): String {
return value.joinToString(separator = ",")
}
@TypeConverter
fun fromStringToIntegerList(value: String): List {
return value.split(",").map { it.toInt() }
}
@TypeConverter
fun fromIntegerListToString(value: List): String {
return value.joinToString(separator = ",")
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/ObjectListTypeConvertor.kt
================================================
package com.vishalgaur.shoppingapp.data.utils
import androidx.room.TypeConverter
import com.google.common.reflect.TypeToken
import com.google.gson.Gson
import com.vishalgaur.shoppingapp.data.UserData
class ObjectListTypeConvertor {
@TypeConverter
fun stringToAddressObjectList(data: String?): List {
if (data.isNullOrBlank()) {
return emptyList()
}
val listType = object : TypeToken>() {}.type
val gson = Gson()
return gson.fromJson(data, listType)
}
@TypeConverter
fun addressObjectListToString(addressList: List): String {
if (addressList.isEmpty()) {
return ""
}
val gson = Gson()
val listType = object : TypeToken>() {}.type
return gson.toJson(addressList, listType)
}
@TypeConverter
fun stringToCartObjectList(data: String?): List {
if (data.isNullOrBlank()) {
return emptyList()
}
val listType = object : TypeToken>() {}.type
val gson = Gson()
return gson.fromJson(data, listType)
}
@TypeConverter
fun cartObjectListToString(cartList: List): String {
if (cartList.isEmpty()) {
return ""
}
val gson = Gson()
val listType = object : TypeToken>() {}.type
return gson.toJson(cartList, listType)
}
@TypeConverter
fun stringToOrderObjectList(data: String?): List {
if (data.isNullOrBlank()) {
return emptyList()
}
val listType = object : TypeToken>() {}.type
val gson = Gson()
return gson.fromJson(data, listType)
}
@TypeConverter
fun orderObjectListToString(orderList: List): String {
if (orderList.isEmpty()) {
return ""
}
val gson = Gson()
val listType = object : TypeToken>() {}.type
return gson.toJson(orderList, listType)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/ProductUtils.kt
================================================
package com.vishalgaur.shoppingapp.data.utils
val ShoeSizes = mapOf(
"UK4" to 4,
"UK5" to 5,
"UK6" to 6,
"UK7" to 7,
"UK8" to 8,
"UK9" to 9,
"UK10" to 10,
"UK11" to 11,
"UK12" to 12
)
val ShoeColors = mapOf(
"black" to "#000000",
"white" to "#FFFFFF",
"red" to "#FF0000",
"green" to "#00FF00",
"blue" to "#0000FF",
"yellow" to "#FFFF00",
"cyan" to "#00FFFF",
"magenta" to "#FF00FF"
)
val ProductCategories = arrayOf("Shoes", "Slippers")
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/data/utils/Utils.kt
================================================
package com.vishalgaur.shoppingapp.data.utils
import java.util.*
enum class SignUpErrors { NONE, SERR }
enum class LogInErrors { NONE, LERR }
enum class AddProductErrors { NONE, ERR_ADD, ERR_ADD_IMG, ADDING }
enum class AddObjectStatus { DONE, ERR_ADD, ADDING }
enum class UserType { CUSTOMER, SELLER }
enum class OrderStatus { CONFIRMED, PACKAGING, PACKED, SHIPPING, SHIPPED, ARRIVING, DELIVERED }
enum class StoreDataStatus { LOADING, ERROR, DONE }
fun getISOCountriesMap(): Map {
val result = mutableMapOf()
val isoCountries = Locale.getISOCountries()
val countriesList = isoCountries.map { isoCountry ->
result[isoCountry] = Locale("", isoCountry).displayCountry
}
return result
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/LaunchActivity.kt
================================================
package com.vishalgaur.shoppingapp.ui
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.ui.loginSignup.LoginSignupActivity
class LaunchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_launch)
setLaunchScreenTimeOut()
}
private fun setLaunchScreenTimeOut() {
Looper.myLooper()?.let {
Handler(it).postDelayed({
startPreferredActivity()
}, TIME_OUT)
}
}
private fun startPreferredActivity() {
val sessionManager = ShoppingAppSessionManager(this)
if (sessionManager.isLoggedIn()) {
launchHome(this)
finish()
} else {
val intent = Intent(this, LoginSignupActivity::class.java)
startActivity(intent)
finish()
}
}
companion object {
private const val TIME_OUT: Long = 1500
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/RecyclerViewPaddingItemDecoration.kt
================================================
package com.vishalgaur.shoppingapp.ui
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class RecyclerViewPaddingItemDecoration(private val context: Context) :
RecyclerView.ItemDecoration() {
private val paddingSpace = 16
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.set(paddingSpace, paddingSpace, paddingSpace, paddingSpace)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/UiUtils.kt
================================================
package com.vishalgaur.shoppingapp.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.getISOCountriesMap
import com.vishalgaur.shoppingapp.ui.home.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.max
enum class SignUpViewErrors { NONE, ERR_EMAIL, ERR_MOBILE, ERR_EMAIL_MOBILE, ERR_EMPTY, ERR_NOT_ACC, ERR_PWD12NS }
enum class LoginViewErrors { NONE, ERR_EMPTY, ERR_MOBILE }
enum class OTPStatus { NONE, CORRECT, WRONG, INVALID_REQ }
enum class AddProductViewErrors { NONE, EMPTY, ERR_PRICE_0 }
enum class AddAddressViewErrors { EMPTY, ERR_FNAME_EMPTY, ERR_LNAME_EMPTY, ERR_STR1_EMPTY, ERR_CITY_EMPTY, ERR_STATE_EMPTY, ERR_ZIP_EMPTY, ERR_ZIP_INVALID, ERR_PHONE_INVALID, ERR_PHONE_EMPTY }
enum class AddItemErrors { ERROR_SIZE, ERROR_COLOR }
class MyOnFocusChangeListener : View.OnFocusChangeListener {
override fun onFocusChange(v: View?, hasFocus: Boolean) {
if (v != null) {
val inputManager =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (!hasFocus) {
inputManager.hideSoftInputFromWindow(v.windowToken, 0)
} else {
inputManager.toggleSoftInputFromWindow(v.windowToken, 0, 0)
}
}
}
}
fun throttleLatest(
intervalMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: (T) -> Unit
): (T) -> Unit {
var throttleJob: Job? = null
var latestParam: T
return { param: T ->
latestParam = param
if (throttleJob?.isCompleted != false) {
throttleJob = coroutineScope.launch {
delay(intervalMs)
latestParam.let(destinationFunction)
}
}
}
}
fun debounce(
waitMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: (T) -> Unit
): (T) -> Unit {
var debounceJob: Job? = null
return { param: T ->
debounceJob?.cancel()
debounceJob = coroutineScope.launch {
delay(waitMs)
destinationFunction(param)
}
}
}
class DotsIndicatorDecoration(
private val radius: Float,
private val indicatorItemPadding: Float,
private val indicatorHeight: Int,
@ColorInt private val colorInactive: Int,
@ColorInt private val colorActive: Int
) : RecyclerView.ItemDecoration() {
private val inactivePaint = Paint()
private val activePaint = Paint()
init {
val width = Resources.getSystem().displayMetrics.density * 1
inactivePaint.apply {
strokeCap = Paint.Cap.ROUND
strokeWidth = width
style = Paint.Style.STROKE
isAntiAlias = true
color = colorInactive
}
activePaint.apply {
strokeCap = Paint.Cap.ROUND
strokeWidth = width
style = Paint.Style.FILL_AND_STROKE
isAntiAlias = true
color = colorActive
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val adapter = parent.adapter ?: return
val itemCount = adapter.itemCount
val totalLength: Float = (radius * 2 * itemCount)
val padBWItems = max(0, itemCount - 1) * indicatorItemPadding
val indicatorTotalWidth = totalLength + padBWItems
val indicatorStartX = (parent.width - indicatorTotalWidth) / 2F
val indicatorPosY = parent.height - indicatorHeight / 2F
drawInactiveDots(c, indicatorStartX, indicatorPosY, itemCount)
val activePos: Int =
(parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
if (activePos == RecyclerView.NO_POSITION) {
return
}
val activeChild =
(parent.layoutManager as LinearLayoutManager).findViewByPosition(activePos)
?: return
drawActiveDot(c, indicatorStartX, indicatorPosY, activePos)
}
private fun drawInactiveDots(
c: Canvas,
indicatorStartX: Float,
indicatorPosY: Float,
itemCount: Int
) {
val w = radius * 2 + indicatorItemPadding
var st = indicatorStartX + radius
for (i in 1..itemCount) {
c.drawCircle(st, indicatorPosY, radius, inactivePaint)
st += w
}
}
private fun drawActiveDot(
c: Canvas,
indicatorStartX: Float,
indicatorPosY: Float,
highlightPos: Int
) {
val w = radius * 2 + indicatorItemPadding
val highStart = indicatorStartX + radius + w * highlightPos
c.drawCircle(highStart, indicatorPosY, radius, activePaint)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.bottom = indicatorHeight
}
}
internal fun launchHome(context: Context) {
val homeIntent = Intent(context, MainActivity::class.java)
homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(homeIntent)
}
internal fun getCompleteAddress(address: UserData.Address): String {
return if (address.streetAddress2.isBlank()) {
"${address.streetAddress}, ${address.city}, ${address.state} - ${address.zipCode}, ${getISOCountriesMap()[address.countryISOCode]}"
} else {
"${address.streetAddress}, ${address.streetAddress2}, ${address.city}, ${address.state} - ${address.zipCode}, ${getISOCountriesMap()[address.countryISOCode]}"
}
}
internal fun disableClickOnWindow(activity: Activity) {
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
)
}
internal fun enableClickOnWindow(activity: Activity) {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AccountFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.databinding.FragmentAccountBinding
import com.vishalgaur.shoppingapp.ui.loginSignup.LoginSignupActivity
import com.vishalgaur.shoppingapp.viewModels.HomeViewModel
private const val TAG = "AccountFragment"
class AccountFragment : Fragment() {
private lateinit var binding: FragmentAccountBinding
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentAccountBinding.inflate(layoutInflater)
setViews()
return binding.root
}
private fun setViews() {
binding.accountTopAppBar.topAppBar.title = getString(R.string.account_fragment_title)
binding.accountProfileTv.setOnClickListener {
Log.d(TAG, "Profile Selected")
findNavController().navigate(R.id.action_accountFragment_to_profileFragment)
}
binding.accountOrdersTv.setOnClickListener {
Log.d(TAG, "Orders Selected")
findNavController().navigate(R.id.action_accountFragment_to_ordersFragment)
}
binding.accountAddressTv.setOnClickListener {
Log.d(TAG, "Address Selected")
findNavController().navigate(R.id.action_accountFragment_to_addressFragment)
}
binding.accountSignOutTv.setOnClickListener {
Log.d(TAG, "Sign Out Selected")
showSignOutDialog()
}
}
private fun showSignOutDialog() {
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.sign_out_dialog_title_text))
.setMessage(getString(R.string.sign_out_dialog_message_text))
.setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(getString(R.string.dialog_sign_out_btn_text)) { dialog, _ ->
viewModel.signOut()
navigateToSignUpActivity()
dialog.cancel()
}
.show()
}
}
private fun navigateToSignUpActivity() {
val homeIntent = Intent(context, LoginSignupActivity::class.java)
homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context?.startActivity(homeIntent)
requireActivity().finish()
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddEditAddressFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.textfield.TextInputLayout
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.utils.AddObjectStatus
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.data.utils.getISOCountriesMap
import com.vishalgaur.shoppingapp.databinding.FragmentAddEditAddressBinding
import com.vishalgaur.shoppingapp.ui.AddAddressViewErrors
import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener
import com.vishalgaur.shoppingapp.viewModels.AddEditAddressViewModel
import java.util.*
import kotlin.properties.Delegates
private const val TAG = "AddAddressFragment"
class AddEditAddressFragment : Fragment() {
private lateinit var binding: FragmentAddEditAddressBinding
private val focusChangeListener = MyOnFocusChangeListener()
private val viewModel by viewModels()
private var isEdit by Delegates.notNull()
private lateinit var addressId: String
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentAddEditAddressBinding.inflate(layoutInflater)
isEdit = arguments?.getBoolean("isEdit") == true
addressId = arguments?.getString("addressId").toString()
initViewModel()
setViews()
setObservers()
return binding.root
}
private fun initViewModel() {
viewModel.setIsEdit(isEdit)
if (isEdit) {
viewModel.setAddressData(addressId)
}
}
private fun setViews() {
if (!isEdit) {
binding.addAddressTopAppBar.topAppBar.title = "Add Address"
} else {
binding.addAddressTopAppBar.topAppBar.title = "Edit Address"
}
binding.addAddressTopAppBar.topAppBar.setNavigationOnClickListener {
findNavController().navigateUp()
}
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.addressFirstNameEditText.onFocusChangeListener = focusChangeListener
binding.addressLastNameEditText.onFocusChangeListener = focusChangeListener
binding.addressStreetAddEditText.onFocusChangeListener = focusChangeListener
binding.addressStreetAdd2EditText.onFocusChangeListener = focusChangeListener
binding.addressCityEditText.onFocusChangeListener = focusChangeListener
binding.addressStateEditText.onFocusChangeListener = focusChangeListener
binding.addressZipcodeEditText.onFocusChangeListener = focusChangeListener
binding.addressPhoneEditText.onFocusChangeListener = focusChangeListener
setCountrySelectTextField()
binding.addAddressSaveBtn.setOnClickListener {
onAddAddress()
if (viewModel.errorStatus.value?.isEmpty() == true) {
viewModel.addAddressStatus.observe(viewLifecycleOwner) { status ->
if (status == AddObjectStatus.DONE) {
makeToast("Address Saved!")
findNavController().navigateUp()
}
}
}
}
}
private fun setObservers() {
viewModel.errorStatus.observe(viewLifecycleOwner) { errList ->
if (errList.isEmpty()) {
binding.addAddressErrorTextView.visibility = View.GONE
} else {
modifyErrors(errList)
}
}
viewModel.dataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> setLoaderState(View.VISIBLE)
StoreDataStatus.ERROR -> {
setLoaderState()
makeToast("Error getting Data, Try Again!")
}
StoreDataStatus.DONE -> {
fillDataInViews()
setLoaderState()
}
else -> {
setLoaderState()
}
}
}
viewModel.addAddressStatus.observe(viewLifecycleOwner) { status ->
when (status) {
AddObjectStatus.DONE -> setLoaderState()
AddObjectStatus.ERR_ADD -> {
setLoaderState()
binding.addAddressErrorTextView.visibility = View.VISIBLE
binding.addAddressErrorTextView.text =
getString(R.string.save_address_error_text)
makeToast(getString(R.string.save_address_error_text))
}
AddObjectStatus.ADDING -> {
setLoaderState(View.VISIBLE)
}
else -> setLoaderState()
}
}
}
private fun fillDataInViews() {
viewModel.addressData.value?.let { address ->
binding.addAddressTopAppBar.topAppBar.title = "Edit Address"
val countryName = getISOCountriesMap()[address.countryISOCode]
binding.addressCountryEditText.setText(countryName, false)
binding.addressFirstNameEditText.setText(address.fName)
binding.addressLastNameEditText.setText(address.lName)
binding.addressStreetAddEditText.setText(address.streetAddress)
binding.addressStreetAdd2EditText.setText(address.streetAddress2)
binding.addressCityEditText.setText(address.city)
binding.addressStateEditText.setText(address.state)
binding.addressZipcodeEditText.setText(address.zipCode)
binding.addressPhoneEditText.setText(address.phoneNumber.substringAfter("+91"))
binding.addAddressSaveBtn.setText(R.string.save_address_btn_text)
}
}
private fun makeToast(errText: String) {
Toast.makeText(context, errText, Toast.LENGTH_LONG).show()
}
private fun setLoaderState(isVisible: Int = View.GONE) {
binding.loaderLayout.loaderFrameLayout.visibility = isVisible
if (isVisible == View.GONE) {
binding.loaderLayout.circularLoader.hideAnimationBehavior
} else {
binding.loaderLayout.circularLoader.showAnimationBehavior
}
}
private fun onAddAddress() {
val countryName = binding.addressCountryEditText.text.toString()
val firstName = binding.addressFirstNameEditText.text.toString()
val lastName = binding.addressLastNameEditText.text.toString()
val streetAdd = binding.addressStreetAddEditText.text.toString()
val streetAdd2 = binding.addressStreetAdd2EditText.text.toString()
val city = binding.addressCityEditText.text.toString()
val state = binding.addressStateEditText.text.toString()
val zipCode = binding.addressZipcodeEditText.text.toString()
val phoneNumber = binding.addressPhoneEditText.text.toString()
val countryCode =
getISOCountriesMap().keys.find { Locale("", it).displayCountry == countryName }
Log.d(TAG, "onAddAddress: Add/Edit Address Initiated")
viewModel.submitAddress(
countryCode!!,
firstName,
lastName,
streetAdd,
streetAdd2,
city,
state,
zipCode,
phoneNumber
)
}
private fun setCountrySelectTextField() {
val isoCountriesMap = getISOCountriesMap()
val countries = isoCountriesMap.values.toSortedSet().toList()
val defaultCountry = Locale.getDefault().displayCountry
val countryAdapter = ArrayAdapter(requireContext(), R.layout.country_list_item, countries)
(binding.addressCountryEditText as? AutoCompleteTextView)?.let {
it.setText(defaultCountry, false)
it.setAdapter(countryAdapter)
}
}
private fun modifyErrors(errList: List) {
binding.fNameOutlinedTextField.error = null
binding.lNameOutlinedTextField.error = null
binding.streetAddOutlinedTextField.error = null
binding.cityOutlinedTextField.error = null
binding.stateOutlinedTextField.error = null
binding.zipCodeOutlinedTextField.error = null
binding.phoneOutlinedTextField.error = null
errList.forEach { err ->
when (err) {
AddAddressViewErrors.EMPTY -> setEditTextsError(true)
AddAddressViewErrors.ERR_FNAME_EMPTY ->
setEditTextsError(true, binding.fNameOutlinedTextField)
AddAddressViewErrors.ERR_LNAME_EMPTY ->
setEditTextsError(true, binding.lNameOutlinedTextField)
AddAddressViewErrors.ERR_STR1_EMPTY ->
setEditTextsError(true, binding.streetAddOutlinedTextField)
AddAddressViewErrors.ERR_CITY_EMPTY ->
setEditTextsError(true, binding.cityOutlinedTextField)
AddAddressViewErrors.ERR_STATE_EMPTY ->
setEditTextsError(true, binding.stateOutlinedTextField)
AddAddressViewErrors.ERR_ZIP_EMPTY ->
setEditTextsError(true, binding.zipCodeOutlinedTextField)
AddAddressViewErrors.ERR_ZIP_INVALID ->
setEditTextsError(false, binding.zipCodeOutlinedTextField)
AddAddressViewErrors.ERR_PHONE_INVALID ->
setEditTextsError(false, binding.phoneOutlinedTextField)
AddAddressViewErrors.ERR_PHONE_EMPTY ->
setEditTextsError(true, binding.phoneOutlinedTextField)
}
}
}
private fun setEditTextsError(isEmpty: Boolean, editText: TextInputLayout? = null) {
if (isEmpty) {
binding.addAddressErrorTextView.visibility = View.VISIBLE
if (editText != null) {
editText.error = "Please Fill the Form"
editText.errorIconDrawable = null
}
} else {
binding.addAddressErrorTextView.visibility = View.GONE
editText!!.error = "Invalid!"
editText.errorIconDrawable = null
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddEditProductFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.res.ColorStateList
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.chip.Chip
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.utils.AddProductErrors
import com.vishalgaur.shoppingapp.data.utils.ShoeColors
import com.vishalgaur.shoppingapp.data.utils.ShoeSizes
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentAddEditProductBinding
import com.vishalgaur.shoppingapp.ui.AddProductViewErrors
import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener
import com.vishalgaur.shoppingapp.viewModels.AddEditProductViewModel
import kotlin.properties.Delegates
private const val TAG = "AddProductFragment"
class AddEditProductFragment : Fragment() {
private lateinit var binding: FragmentAddEditProductBinding
private val viewModel by viewModels()
private val focusChangeListener = MyOnFocusChangeListener()
// arguments
private var isEdit by Delegates.notNull()
private lateinit var catName: String
private lateinit var productId: String
private var sizeList = mutableSetOf()
private var colorsList = mutableSetOf()
private var imgList = mutableListOf()
private val getImages =
registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { result ->
imgList.addAll(result)
if (imgList.size > 3) {
imgList = imgList.subList(0, 3)
makeToast("Maximum 3 images are allowed!")
}
val adapter = context?.let { AddProductImagesAdapter(it, imgList) }
binding.addProImagesRv.adapter = adapter
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentAddEditProductBinding.inflate(layoutInflater)
isEdit = arguments?.getBoolean("isEdit") == true
catName = arguments?.getString("categoryName").toString()
productId = arguments?.getString("productId").toString()
initViewModel()
setViews()
setObservers()
return binding.root
}
private fun initViewModel() {
Log.d(TAG, "init view model, isedit = $isEdit")
viewModel.setIsEdit(isEdit)
if (isEdit) {
Log.d(TAG, "init view model, isedit = true, $productId")
viewModel.setProductData(productId)
} else {
Log.d(TAG, "init view model, isedit = false, $catName")
viewModel.setCategory(catName)
}
}
private fun setObservers() {
viewModel.errorStatus.observe(viewLifecycleOwner) { err ->
modifyErrors(err)
}
viewModel.dataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
}
StoreDataStatus.DONE -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
fillDataInAllViews()
}
else -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
makeToast("Error getting Data, Try Again!")
}
}
}
viewModel.addProductErrors.observe(viewLifecycleOwner) { status ->
when (status) {
AddProductErrors.ADDING -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
}
AddProductErrors.ERR_ADD_IMG -> {
setAddProductErrors(getString(R.string.add_product_error_img_upload))
}
AddProductErrors.ERR_ADD -> {
setAddProductErrors(getString(R.string.add_product_insert_error))
}
AddProductErrors.NONE -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
}
}
}
}
private fun setAddProductErrors(errText: String) {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.addProErrorTextView.visibility = View.VISIBLE
binding.addProErrorTextView.text = errText
}
private fun fillDataInAllViews() {
viewModel.productData.value?.let { product ->
Log.d(TAG, "fill data in views")
binding.addProAppBar.topAppBar.title = "Edit Product - ${product.name}"
binding.proNameEditText.setText(product.name)
binding.proPriceEditText.setText(product.price.toString())
binding.proMrpEditText.setText(product.mrp.toString())
binding.proDescEditText.setText(product.description)
imgList = product.images.map { it.toUri() } as MutableList
val adapter = AddProductImagesAdapter(requireContext(), imgList)
binding.addProImagesRv.adapter = adapter
setShoeSizesChips(product.availableSizes)
setShoeColorsChips(product.availableColors)
binding.addProBtn.setText(R.string.edit_product_btn_text)
}
}
private fun setViews() {
Log.d(TAG, "set views")
if (!isEdit) {
binding.addProAppBar.topAppBar.title =
"Add Product - ${viewModel.selectedCategory.value}"
val adapter = AddProductImagesAdapter(requireContext(), imgList)
binding.addProImagesRv.adapter = adapter
}
binding.addProImagesBtn.setOnClickListener {
getImages.launch("image/*")
}
binding.addProAppBar.topAppBar.setNavigationOnClickListener {
findNavController().navigateUp()
}
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
setShoeSizesChips()
setShoeColorsChips()
binding.addProErrorTextView.visibility = View.GONE
binding.proNameEditText.onFocusChangeListener = focusChangeListener
binding.proPriceEditText.onFocusChangeListener = focusChangeListener
binding.proMrpEditText.onFocusChangeListener = focusChangeListener
binding.proDescEditText.onFocusChangeListener = focusChangeListener
binding.addProBtn.setOnClickListener {
onAddProduct()
if (viewModel.errorStatus.value == AddProductViewErrors.NONE) {
viewModel.addProductErrors.observe(viewLifecycleOwner) { err ->
if (err == AddProductErrors.NONE) {
findNavController().navigate(R.id.action_addProductFragment_to_homeFragment)
}
}
}
}
}
private fun onAddProduct() {
val name = binding.proNameEditText.text.toString()
val price = binding.proPriceEditText.text.toString().toDoubleOrNull()
val mrp = binding.proMrpEditText.text.toString().toDoubleOrNull()
val desc = binding.proDescEditText.text.toString()
Log.d(
TAG,
"onAddProduct: Add product initiated, $name, $price, $mrp, $desc, $sizeList, $colorsList, $imgList"
)
viewModel.submitProduct(
name, price, mrp, desc, sizeList.toList(), colorsList.toList(), imgList
)
}
private fun setShoeSizesChips(shoeList: List? = emptyList()) {
binding.addProSizeChipGroup.apply {
removeAllViews()
for ((_, v) in ShoeSizes) {
val chip = Chip(context)
chip.id = v
chip.tag = v
chip.text = "$v"
chip.isCheckable = true
if (shoeList?.contains(v) == true) {
chip.isChecked = true
sizeList.add(chip.tag.toString().toInt())
}
chip.setOnCheckedChangeListener { buttonView, isChecked ->
val tag = buttonView.tag.toString().toInt()
if (!isChecked) {
sizeList.remove(tag)
} else {
sizeList.add(tag)
}
}
addView(chip)
}
invalidate()
}
}
private fun setShoeColorsChips(colorList: List? = emptyList()) {
binding.addProColorChipGroup.apply {
removeAllViews()
var ind = 1
for ((k, v) in ShoeColors) {
val chip = Chip(context)
chip.id = ind
chip.tag = k
chip.chipStrokeColor = ColorStateList.valueOf(Color.BLACK)
chip.chipStrokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1F,
context.resources.displayMetrics
)
chip.chipBackgroundColor = ColorStateList.valueOf(Color.parseColor(v))
chip.isCheckable = true
if (colorList?.contains(k) == true) {
chip.isChecked = true
colorsList.add(chip.tag.toString())
}
chip.setOnCheckedChangeListener { buttonView, isChecked ->
val tag = buttonView.tag.toString()
if (!isChecked) {
colorsList.remove(tag)
} else {
colorsList.add(tag)
}
}
addView(chip)
ind++
}
invalidate()
}
}
private fun modifyErrors(err: AddProductViewErrors) {
when (err) {
AddProductViewErrors.NONE -> binding.addProErrorTextView.visibility = View.GONE
AddProductViewErrors.EMPTY -> {
binding.addProErrorTextView.visibility = View.VISIBLE
binding.addProErrorTextView.text = getString(R.string.add_product_error_string)
}
AddProductViewErrors.ERR_PRICE_0 -> {
binding.addProErrorTextView.visibility = View.VISIBLE
binding.addProErrorTextView.text = getString(R.string.add_pro_error_price_string)
}
}
}
private fun makeToast(text: String) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddProductImagesAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Context
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.vishalgaur.shoppingapp.databinding.AddImagesItemBinding
class AddProductImagesAdapter(private val context: Context, images: List) :
RecyclerView.Adapter() {
private var data: MutableList = images as MutableList
inner class ViewHolder(private var binding: AddImagesItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(imgUrl: Uri, pos: Int) {
binding.addImgCloseBtn.setOnClickListener {
deleteItem(pos)
}
if (imgUrl.toString().contains("https://")) {
Glide.with(context)
.asBitmap()
.load(imgUrl.buildUpon().scheme("https").build())
.into(binding.addImagesImageView)
} else {
binding.addImagesImageView.setImageURI(imgUrl)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
AddImagesItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val imageUrl = data[position]
holder.bind(imageUrl, position)
}
override fun getItemCount(): Int = data.size
fun deleteItem(index: Int) {
data.removeAt(index)
notifyDataSetChanged()
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddressAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Context
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.databinding.LayoutAddressCardBinding
import com.vishalgaur.shoppingapp.ui.getCompleteAddress
private const val TAG = "AddressAdapter"
class AddressAdapter(
private val context: Context,
addresses: List,
private val isSelect: Boolean
) :
RecyclerView.Adapter() {
lateinit var onClickListener: OnClickListener
var data: List = addresses
var lastCheckedAddress: String? = null
private var lastCheckedCard: MaterialCardView? = null
var selectedAddressPos = -1
inner class ViewHolder(private var binding: LayoutAddressCardBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(address: UserData.Address, position: Int) {
binding.addressCard.isChecked = position == selectedAddressPos
binding.addressPersonNameTv.text =
context.getString(R.string.person_name, address.fName, address.lName)
binding.addressCompleteAddressTv.text = getCompleteAddress(address)
binding.addressMobileTv.text = address.phoneNumber
if (isSelect) {
binding.addressCard.setOnClickListener {
onCardClick(position, address.addressId, it as MaterialCardView)
}
}
binding.addressEditBtn.setOnClickListener {
onClickListener.onEditClick(address.addressId)
}
binding.addressDeleteBtn.setOnClickListener {
onClickListener.onDeleteClick(address.addressId)
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutAddressCardBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position], position)
}
override fun getItemCount(): Int = data.size
interface OnClickListener {
fun onEditClick(addressId: String)
fun onDeleteClick(addressId: String)
}
private fun onCardClick(position: Int, addressTd: String, card: MaterialCardView) {
if (addressTd != lastCheckedAddress) {
card.apply {
strokeColor = context.getColor(R.color.blue_accent_300)
isChecked = true
strokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
2F,
resources.displayMetrics
).toInt()
}
lastCheckedCard?.apply {
strokeColor = context.getColor(R.color.light_gray)
isChecked = false
strokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1F,
resources.displayMetrics
).toInt()
}
lastCheckedAddress = addressTd
lastCheckedCard = card
selectedAddressPos = position
Log.d(TAG, "onCardClick: selected address = $addressTd")
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/AddressFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentAddressBinding
import com.vishalgaur.shoppingapp.viewModels.HomeViewModel
private const val TAG = "AddressFragment"
class AddressFragment : Fragment() {
private lateinit var binding: FragmentAddressBinding
private lateinit var addressAdapter: AddressAdapter
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentAddressBinding.inflate(layoutInflater)
setViews()
setObservers()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getUserAddresses()
}
private fun setViews() {
binding.addressAppBar.topAppBar.title = "Address"
binding.addressAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() }
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.addressAddBtn.visibility = View.GONE
binding.addressAddBtn.setOnClickListener {
navigateToAddEditAddress(false)
}
binding.addressEmptyTextView.visibility = View.GONE
if (context != null) {
val addressList = viewModel.userAddresses.value ?: emptyList()
addressAdapter = AddressAdapter(requireContext(), addressList, false)
addressAdapter.onClickListener = object : AddressAdapter.OnClickListener {
override fun onEditClick(addressId: String) {
Log.d(TAG, "onEditAddress: initiated")
navigateToAddEditAddress(true, addressId)
}
override fun onDeleteClick(addressId: String) {
Log.d(TAG, "onDeleteAddress: initiated")
showDeleteDialog(addressId)
}
}
binding.addressAddressesRecyclerView.adapter = addressAdapter
}
}
private fun setObservers() {
viewModel.dataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.addressEmptyTextView.visibility = View.GONE
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
}
else -> {
binding.addressAddBtn.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
}
}
if (status != null && status != StoreDataStatus.LOADING) {
viewModel.userAddresses.observe(viewLifecycleOwner) { addressList ->
if (addressList.isNotEmpty()) {
addressAdapter.data = addressList
binding.addressAddressesRecyclerView.adapter = addressAdapter
binding.addressAddressesRecyclerView.adapter?.notifyDataSetChanged()
} else if (addressList.isEmpty()) {
binding.addressAddressesRecyclerView.visibility = View.GONE
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.addressEmptyTextView.visibility = View.VISIBLE
}
}
binding.addressAddBtn.visibility = View.VISIBLE
}
}
}
private fun showDeleteDialog(addressId: String) {
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.delete_dialog_title_text))
.setMessage(getString(R.string.delete_address_message_text))
.setNeutralButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ ->
viewModel.deleteAddress(addressId)
dialog.cancel()
}
.show()
}
}
private fun navigateToAddEditAddress(isEdit: Boolean, addressId: String? = null) {
findNavController().navigate(
R.id.action_addressFragment_to_addEditAddressFragment,
bundleOf("isEdit" to isEdit, "addressId" to addressId)
)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/CartFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentCartBinding
import com.vishalgaur.shoppingapp.databinding.LayoutCircularLoaderBinding
import com.vishalgaur.shoppingapp.databinding.LayoutPriceCardBinding
import com.vishalgaur.shoppingapp.viewModels.OrderViewModel
private const val TAG = "CartFragment"
class CartFragment : Fragment() {
private lateinit var binding: FragmentCartBinding
private val orderViewModel: OrderViewModel by activityViewModels()
private lateinit var itemsAdapter: CartItemAdapter
private lateinit var concatAdapter: ConcatAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentCartBinding.inflate(layoutInflater)
setViews()
setObservers()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
orderViewModel.getUserLikes()
orderViewModel.getCartItems()
}
private fun setViews() {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
binding.cartAppBar.topAppBar.title = getString(R.string.cart_fragment_label)
binding.cartEmptyTextView.visibility = View.GONE
binding.cartCheckOutBtn.setOnClickListener {
navigateToSelectAddress()
}
if (context != null) {
setItemsAdapter(orderViewModel.cartItems.value)
concatAdapter = ConcatAdapter(itemsAdapter, PriceCardAdapter())
binding.cartProductsRecyclerView.adapter = concatAdapter
}
}
private fun setObservers() {
orderViewModel.dataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.cartProductsRecyclerView.visibility = View.GONE
binding.cartCheckOutBtn.visibility = View.GONE
binding.cartEmptyTextView.visibility = View.GONE
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
}
else -> {
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
}
}
}
orderViewModel.dataStatus.observe(viewLifecycleOwner) { status ->
if (status != null && status != StoreDataStatus.LOADING) {
orderViewModel.cartProducts.observe(viewLifecycleOwner) { itemList ->
if (itemList.isNotEmpty()) {
updateAdapter()
binding.cartEmptyTextView.visibility = View.GONE
binding.cartProductsRecyclerView.visibility = View.VISIBLE
binding.cartCheckOutBtn.visibility = View.VISIBLE
} else if (itemList.isEmpty()) {
binding.cartProductsRecyclerView.visibility = View.GONE
binding.cartCheckOutBtn.visibility = View.GONE
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.cartEmptyTextView.visibility = View.VISIBLE
}
}
}
}
orderViewModel.cartItems.observe(viewLifecycleOwner) { items ->
if (items.isNotEmpty()) {
updateAdapter()
}
}
orderViewModel.priceList.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
updateAdapter()
}
}
orderViewModel.userLikes.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
updateAdapter()
}
}
}
private fun updateAdapter() {
val items = orderViewModel.cartItems.value ?: emptyList()
val likeList = orderViewModel.userLikes.value ?: emptyList()
val prosList = orderViewModel.cartProducts.value ?: emptyList()
itemsAdapter.apply {
data = items
proList = prosList
likesList = likeList
}
concatAdapter = ConcatAdapter(itemsAdapter, PriceCardAdapter())
binding.cartProductsRecyclerView.adapter = concatAdapter
binding.cartProductsRecyclerView.adapter?.notifyDataSetChanged()
}
private fun setItemsAdapter(itemList: List?) {
val items = itemList ?: emptyList()
val likesList = orderViewModel.userLikes.value ?: emptyList()
val proList = orderViewModel.cartProducts.value ?: emptyList()
itemsAdapter = CartItemAdapter(requireContext(), items, proList, likesList)
itemsAdapter.onClickListener = object : CartItemAdapter.OnClickListener {
override fun onLikeClick(productId: String) {
Log.d(TAG, "onToggle Like Clicked")
orderViewModel.toggleLikeProduct(productId)
}
override fun onDeleteClick(itemId: String, itemBinding: LayoutCircularLoaderBinding) {
Log.d(TAG, "onDelete: initiated")
showDeleteDialog(itemId, itemBinding)
}
override fun onPlusClick(itemId: String) {
Log.d(TAG, "onPlus: Increasing quantity")
orderViewModel.setQuantityOfItem(itemId, 1)
}
override fun onMinusClick(itemId: String, currQuantity: Int,itemBinding: LayoutCircularLoaderBinding) {
Log.d(TAG, "onMinus: decreasing quantity")
if (currQuantity == 1) {
showDeleteDialog(itemId, itemBinding)
} else {
orderViewModel.setQuantityOfItem(itemId, -1)
}
}
}
}
private fun navigateToSelectAddress() {
findNavController().navigate(R.id.action_cartFragment_to_selectAddressFragment)
}
private fun showDeleteDialog(itemId: String, itemBinding: LayoutCircularLoaderBinding) {
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.delete_dialog_title_text))
.setMessage(getString(R.string.delete_cart_item_message_text))
.setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ ->
dialog.cancel()
itemBinding.loaderFrameLayout.visibility = View.GONE
}
.setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ ->
orderViewModel.deleteItemFromCart(itemId)
dialog.cancel()
}.setOnCancelListener {
itemBinding.loaderFrameLayout.visibility = View.GONE
}
.show()
}
}
inner class PriceCardAdapter : RecyclerView.Adapter() {
inner class ViewHolder(private val priceCardBinding: LayoutPriceCardBinding) :
RecyclerView.ViewHolder(priceCardBinding.root) {
fun bind() {
priceCardBinding.priceItemsLabelTv.text = getString(
R.string.price_card_items_string,
orderViewModel.getItemsCount().toString()
)
priceCardBinding.priceItemsAmountTv.text =
getString(R.string.price_text, orderViewModel.getItemsPriceTotal().toString())
priceCardBinding.priceShippingAmountTv.text = getString(R.string.price_text, "0")
priceCardBinding.priceChargesAmountTv.text = getString(R.string.price_text, "0")
priceCardBinding.priceTotalAmountTv.text =
getString(R.string.price_text, orderViewModel.getItemsPriceTotal().toString())
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutPriceCardBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind()
}
override fun getItemCount() = 1
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/CartItemAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.databinding.CartListItemBinding
import com.vishalgaur.shoppingapp.databinding.LayoutCircularLoaderBinding
class CartItemAdapter(
private val context: Context, items: List,
products: List, userLikes: List
) : RecyclerView.Adapter() {
lateinit var onClickListener: OnClickListener
var data: List = items
var proList: List = products
var likesList: List = userLikes
inner class ViewHolder(private val binding: CartListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(itemData: UserData.CartItem) {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
val proData = proList.find { it.productId == itemData.productId } ?: Product()
binding.cartProductTitleTv.text = proData.name
binding.cartProductPriceTv.text =
context.getString(R.string.price_text, proData.price.toString())
if (proData.images.isNotEmpty()) {
val imgUrl = proData.images[0].toUri().buildUpon().scheme("https").build()
Glide.with(context)
.asBitmap()
.load(imgUrl)
.into(binding.productImageView)
binding.productImageView.clipToOutline = true
}
binding.cartProductQuantityTextView.text = itemData.quantity.toString()
if (likesList.contains(proData.productId)) {
binding.cartProductLikeBtn.setImageResource(R.drawable.liked_heart_drawable)
} else {
binding.cartProductLikeBtn.setImageResource(R.drawable.heart_icon_drawable)
}
binding.cartProductLikeBtn.setOnClickListener {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
if (!likesList.contains(proData.productId)) {
binding.cartProductLikeBtn.setImageResource(R.drawable.liked_heart_drawable)
} else {
binding.cartProductLikeBtn.setImageResource(R.drawable.heart_icon_drawable)
}
onClickListener.onLikeClick(proData.productId)
}
binding.cartProductDeleteBtn.setOnClickListener {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
onClickListener.onDeleteClick(itemData.itemId, binding.loaderLayout)
}
binding.cartProductPlusBtn.setOnClickListener {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
onClickListener.onPlusClick(itemData.itemId)
}
binding.cartProductMinusBtn.setOnClickListener {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
onClickListener.onMinusClick(itemData.itemId, itemData.quantity, binding.loaderLayout)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
CartListItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount() = data.size
interface OnClickListener {
fun onLikeClick(productId: String)
fun onDeleteClick(itemId: String, itemBinding: LayoutCircularLoaderBinding)
fun onPlusClick(itemId: String)
fun onMinusClick(itemId: String, currQuantity: Int, itemBinding: LayoutCircularLoaderBinding)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/FavoritesFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentFavoritesBinding
import com.vishalgaur.shoppingapp.ui.RecyclerViewPaddingItemDecoration
import com.vishalgaur.shoppingapp.viewModels.HomeViewModel
private const val TAG = "FavoritesFragment"
class FavoritesFragment : Fragment() {
private lateinit var binding: FragmentFavoritesBinding
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var productsAdapter: LikedProductAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFavoritesBinding.inflate(layoutInflater)
setViews()
setObservers()
return binding.root
}
private fun setViews() {
viewModel.setDataLoading()
viewModel.getLikedProducts()
binding.favTopAppBar.topAppBar.title = "Favorite Products"
binding.favTopAppBar.topAppBar.setNavigationOnClickListener {
findNavController().navigateUp()
}
binding.favEmptyTextView.visibility = View.GONE
if (context != null) {
val proList = viewModel.likedProducts.value ?: emptyList()
productsAdapter = LikedProductAdapter(proList, requireContext())
productsAdapter.onClickListener = object : LikedProductAdapter.OnClickListener {
override fun onClick(productData: Product) {
Log.d(TAG, "Product: ${productData.productId} clicked")
findNavController().navigate(
R.id.action_favoritesFragment_to_productDetailsFragment,
bundleOf("productId" to productData.productId)
)
}
override fun onDeleteClick(productId: String) {
viewModel.toggleLikeByProductId(productId)
}
}
binding.favProductsRecyclerView.apply {
val itemDecoration = RecyclerViewPaddingItemDecoration(requireContext())
if (itemDecorationCount == 0) {
addItemDecoration(itemDecoration)
}
}
}
}
private fun setObservers() {
viewModel.dataStatus.observe(viewLifecycleOwner) { status ->
if (status == StoreDataStatus.LOADING) {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
binding.favEmptyTextView.visibility = View.GONE
} else if (status != null) {
viewModel.likedProducts.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
productsAdapter.data = viewModel.likedProducts.value!!
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
productsAdapter.data = it
binding.favProductsRecyclerView.adapter = productsAdapter
binding.favProductsRecyclerView.adapter?.apply {
notifyDataSetChanged()
}
} else if (it.isEmpty()) {
binding.favEmptyTextView.visibility = View.VISIBLE
binding.favProductsRecyclerView.visibility = View.GONE
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.loaderLayout.circularLoader.hideAnimationBehavior
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/HomeFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.CheckBox
import android.widget.ImageView
import androidx.core.os.bundleOf
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.utils.ProductCategories
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentHomeBinding
import com.vishalgaur.shoppingapp.ui.MyOnFocusChangeListener
import com.vishalgaur.shoppingapp.ui.RecyclerViewPaddingItemDecoration
import com.vishalgaur.shoppingapp.viewModels.HomeViewModel
import kotlinx.coroutines.*
private const val TAG = "HomeFragment"
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: HomeViewModel by activityViewModels()
private val focusChangeListener = MyOnFocusChangeListener()
private lateinit var productAdapter: ProductAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentHomeBinding.inflate(layoutInflater)
setViews()
setObservers()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getUserLikes()
}
// override fun onResume() {
// super.onResume()
// viewModel.getLikedProducts()
// }
private fun setViews() {
setHomeTopAppBar()
if (context != null) {
setProductsAdapter(viewModel.products.value)
binding.productsRecyclerView.apply {
val gridLayoutManager = GridLayoutManager(context, 2)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (productAdapter.getItemViewType(position)) {
2 -> 2 //ad
else -> {
val proCount = productAdapter.data.count { it is Product }
val adCount = productAdapter.data.size - proCount
val totalCount = proCount + (adCount * 2)
// product, full for last item
if (position + 1 == productAdapter.data.size && totalCount % 2 == 1) 2 else 1
}
}
}
}
layoutManager = gridLayoutManager
adapter = productAdapter
val itemDecoration = RecyclerViewPaddingItemDecoration(requireContext())
if (itemDecorationCount == 0) {
addItemDecoration(itemDecoration)
}
}
}
if (!viewModel.isUserASeller) {
binding.homeFabAddProduct.visibility = View.GONE
}
binding.homeFabAddProduct.setOnClickListener {
showDialogWithItems(ProductCategories, 0, false)
}
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
}
private fun setObservers() {
viewModel.storeDataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
binding.productsRecyclerView.visibility = View.GONE
}
else -> {
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
}
}
if (status != null && status != StoreDataStatus.LOADING) {
viewModel.products.observe(viewLifecycleOwner) { productsList ->
if (productsList.isNotEmpty()) {
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.productsRecyclerView.visibility = View.VISIBLE
binding.productsRecyclerView.adapter?.apply {
productAdapter.data =
getMixedDataList(productsList, getAdsList())
notifyDataSetChanged()
}
}
}
}
}
viewModel.allProducts.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
viewModel.setDataLoaded()
viewModel.filterProducts("All")
}
}
viewModel.userLikes.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.productsRecyclerView.adapter?.apply {
notifyDataSetChanged()
}
}
}
}
private fun performSearch(query: String) {
viewModel.filterBySearch(query)
}
private fun setAppBarItemClicks(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.home_filter -> {
val extraFilters = arrayOf("All", "None")
val categoryList = ProductCategories.plus(extraFilters)
val checkedItem = categoryList.indexOf(viewModel.filterCategory.value)
showDialogWithItems(categoryList, checkedItem, true)
true
}
R.id.home_favorites -> {
// show favorite products list
findNavController().navigate(R.id.action_homeFragment_to_favoritesFragment)
true
}
else -> false
}
}
private fun setHomeTopAppBar() {
var lastInput = ""
val debounceJob: Job? = null
val uiScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
binding.homeTopAppBar.topAppBar.inflateMenu(R.menu.home_app_bar_menu)
if (viewModel.isUserASeller) {
binding.homeTopAppBar.topAppBar.menu.removeItem(R.id.home_favorites)
}
binding.homeTopAppBar.homeSearchEditText.onFocusChangeListener = focusChangeListener
binding.homeTopAppBar.homeSearchEditText.doAfterTextChanged { editable ->
if (editable != null) {
val newtInput = editable.toString()
debounceJob?.cancel()
if (lastInput != newtInput) {
lastInput = newtInput
uiScope.launch {
delay(500)
if (lastInput == newtInput) {
performSearch(newtInput)
}
}
}
}
}
binding.homeTopAppBar.homeSearchEditText.setOnEditorActionListener { textView, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
textView.clearFocus()
val inputManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(textView.windowToken, 0)
performSearch(textView.text.toString())
true
} else {
false
}
}
binding.homeTopAppBar.searchOutlinedTextLayout.setEndIconOnClickListener {
it.clearFocus()
binding.homeTopAppBar.homeSearchEditText.setText("")
val inputManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(it.windowToken, 0)
// viewModel.filterProducts("All")
}
binding.homeTopAppBar.topAppBar.setOnMenuItemClickListener { menuItem ->
setAppBarItemClicks(menuItem)
}
}
private fun setProductsAdapter(productsList: List?) {
val likesList = viewModel.userLikes.value ?: emptyList()
productAdapter = ProductAdapter(productsList ?: emptyList(), likesList, requireContext())
productAdapter.onClickListener = object : ProductAdapter.OnClickListener {
override fun onClick(productData: Product) {
findNavController().navigate(
R.id.action_seeProduct,
bundleOf("productId" to productData.productId)
)
}
override fun onDeleteClick(productData: Product) {
Log.d(TAG, "onDeleteProduct: initiated for ${productData.productId}")
showDeleteDialog(productData.name, productData.productId)
}
override fun onEditClick(productId: String) {
Log.d(TAG, "onEditProduct: initiated for $productId")
navigateToAddEditProductFragment(isEdit = true, productId = productId)
}
override fun onLikeClick(productId: String) {
Log.d(TAG, "onToggleLike: initiated for $productId")
viewModel.toggleLikeByProductId(productId)
}
override fun onAddToCartClick(productData: Product) {
Log.d(TAG, "onToggleCartAddition: initiated")
viewModel.toggleProductInCart(productData)
}
}
productAdapter.bindImageButtons = object : ProductAdapter.BindImageButtons {
@SuppressLint("ResourceAsColor")
override fun setLikeButton(productId: String, button: CheckBox) {
button.isChecked = viewModel.isProductLiked(productId)
}
override fun setCartButton(productId: String, imgView: ImageView) {
if (viewModel.isProductInCart(productId)) {
imgView.setImageResource(R.drawable.ic_remove_shopping_cart_24)
} else {
imgView.setImageResource(R.drawable.ic_add_shopping_cart_24)
}
}
}
}
private fun showDeleteDialog(productName: String, productId: String) {
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.delete_dialog_title_text))
.setMessage(getString(R.string.delete_dialog_message_text, productName))
.setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(getString(R.string.delete_dialog_delete_btn_text)) { dialog, _ ->
viewModel.deleteProduct(productId)
dialog.cancel()
}
.show()
}
}
private fun showDialogWithItems(
categoryItems: Array,
checkedOption: Int = 0,
isFilter: Boolean
) {
var checkedItem = checkedOption
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.pro_cat_dialog_title))
.setSingleChoiceItems(categoryItems, checkedItem) { _, which ->
checkedItem = which
}
.setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(getString(R.string.pro_cat_dialog_ok_btn)) { dialog, _ ->
if (checkedItem == -1) {
dialog.cancel()
} else {
if (isFilter) {
viewModel.filterProducts(categoryItems[checkedItem])
} else {
navigateToAddEditProductFragment(
isEdit = false,
catName = categoryItems[checkedItem]
)
}
}
dialog.cancel()
}
.show()
}
}
private fun navigateToAddEditProductFragment(
isEdit: Boolean,
catName: String? = null,
productId: String? = null
) {
findNavController().navigate(
R.id.action_goto_addProduct,
bundleOf("isEdit" to isEdit, "categoryName" to catName, "productId" to productId)
)
}
private fun getMixedDataList(data: List, adsList: List): List {
val itemsList = mutableListOf()
itemsList.addAll(data.sortedBy { it.productId })
var currPos = 0
if (itemsList.size >= 4) {
adsList.forEach label@{ ad ->
if (itemsList.size > currPos + 1) {
itemsList.add(currPos, ad)
} else {
return@label
}
currPos += 5
}
}
return itemsList
}
private fun getAdsList(): List {
return listOf(R.drawable.ad_ex_2, R.drawable.ad_ex_1, R.drawable.ad_ex_3)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/LikedProductAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Context
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.databinding.ProductsListItemBinding
import com.vishalgaur.shoppingapp.getOfferPercentage
class LikedProductAdapter(proList: List, private val context: Context) :
RecyclerView.Adapter() {
var data = proList
lateinit var onClickListener: OnClickListener
inner class ViewHolder(private val binding: ProductsListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(productData: Product) {
binding.productCard.setOnClickListener {
onClickListener.onClick(productData)
}
binding.productNameTv.text = productData.name
binding.productPriceTv.text =
context.getString(R.string.pro_details_price_value, productData.price.toString())
binding.productRatingBar.rating = productData.rating.toFloat()
binding.productActualPriceTv.apply {
paintFlags = Paint.STRIKE_THRU_TEXT_FLAG
text = context.getString(
R.string.pro_details_actual_strike_value,
productData.mrp.toString()
)
}
binding.productOfferValueTv.text = context.getString(
R.string.pro_offer_precent_text,
getOfferPercentage(productData.mrp, productData.price).toString()
)
if (productData.images.isNotEmpty()) {
val imgUrl = productData.images[0].toUri().buildUpon().scheme("https").build()
Glide.with(context)
.asBitmap()
.load(imgUrl)
.into(binding.productImageView)
binding.productImageView.clipToOutline = true
}
//hiding unnecessary button
binding.productAddToCartButton.visibility = View.GONE
binding.productDeleteButton.visibility = View.GONE
binding.productLikeCheckbox.visibility = View.GONE
// setting edit button as delete button
binding.productEditButton.setImageResource(R.drawable.ic_delete_24)
binding.productEditButton.setOnClickListener {
onClickListener.onDeleteClick(productData.productId)
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ProductsListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount() = data.size
interface OnClickListener {
fun onClick(productData: Product)
fun onDeleteClick(productId: String)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/MainActivity.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.ShoppingAppSessionManager
import com.vishalgaur.shoppingapp.databinding.ActivityMainBinding
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate starts")
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Bottom Navigation
setUpNav()
}
private fun setUpNav() {
val navFragment =
supportFragmentManager.findFragmentById(R.id.home_nav_host_fragment) as NavHostFragment
NavigationUI.setupWithNavController(binding.homeBottomNavigation, navFragment.navController)
navFragment.navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.homeFragment -> setBottomNavVisibility(View.VISIBLE)
R.id.cartFragment -> setBottomNavVisibility(View.VISIBLE)
R.id.accountFragment -> setBottomNavVisibility(View.VISIBLE)
R.id.ordersFragment -> setBottomNavVisibility(View.VISIBLE)
R.id.orderSuccessFragment -> setBottomNavVisibility(View.VISIBLE)
else -> setBottomNavVisibility(View.GONE)
}
}
val sessionManager = ShoppingAppSessionManager(this.applicationContext)
if (sessionManager.isUserSeller()) {
binding.homeBottomNavigation.menu.removeItem(R.id.cartFragment)
}else {
binding.homeBottomNavigation.menu.removeItem(R.id.ordersFragment)
}
}
private fun setBottomNavVisibility(visibility: Int) {
binding.homeBottomNavigation.visibility = visibility
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrderDetailsFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.data.utils.OrderStatus
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentOrderDetailsBinding
import com.vishalgaur.shoppingapp.ui.getCompleteAddress
import com.vishalgaur.shoppingapp.viewModels.HomeViewModel
import java.time.Month
import java.util.*
class OrderDetailsFragment : Fragment() {
private lateinit var binding: FragmentOrderDetailsBinding
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var orderId: String
private lateinit var productsAdapter: OrderProductsAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentOrderDetailsBinding.inflate(layoutInflater)
orderId = arguments?.getString("orderId").toString()
viewModel.getOrderDetailsByOrderId(orderId)
setViews()
setObservers()
return binding.root
}
private fun setViews() {
binding.orderDetailAppBar.topAppBar.title = getString(R.string.order_details_fragment_title)
binding.orderDetailAppBar.topAppBar.setNavigationOnClickListener { findNavController().navigateUp() }
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.orderDetailsConstraintGroup.visibility = View.GONE
if (context != null) {
setProductsAdapter(viewModel.selectedOrder.value?.items)
binding.orderDetailsProRecyclerView.adapter = productsAdapter
}
}
private fun setObservers() {
viewModel.storeDataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
binding.orderDetailsConstraintGroup.visibility = View.GONE
}
else -> {
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
}
}
}
viewModel.selectedOrder.observe(viewLifecycleOwner) { orderData ->
if (orderData != null) {
binding.orderDetailsConstraintGroup.visibility = View.VISIBLE
setAllViews(orderData)
val items = orderData.items
val likeList = viewModel.userLikes.value ?: emptyList()
val prosList = viewModel.orderProducts.value ?: emptyList()
productsAdapter.apply {
data = items
proList = prosList
likesList = likeList
}
binding.orderDetailsProRecyclerView.adapter = productsAdapter
binding.orderDetailsProRecyclerView.adapter?.notifyDataSetChanged()
} else {
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
binding.orderDetailsConstraintGroup.visibility = View.GONE
}
}
}
private fun setAllViews(orderData: UserData.OrderItem) {
Log.d("OrderDetail", "set all views called")
if (viewModel.isUserASeller) {
binding.orderChangeStatusBtn.visibility = View.VISIBLE
binding.orderChangeStatusBtn.setOnClickListener {
val statusString = orderData.status.split(" ")[0]
val pos = OrderStatus.values().map { it.name }.indexOf(statusString)
showDialogWithItems(pos, orderData.orderId)
}
} else {
binding.orderChangeStatusBtn.visibility = View.GONE
}
val calendar = Calendar.getInstance()
calendar.time = orderData.orderDate
binding.orderDetailsShippingAddLayout.shipDateValueTv.text = getString(
R.string.order_date_text,
Month.values()[(calendar.get(Calendar.MONTH))].name,
calendar.get(Calendar.DAY_OF_MONTH).toString(),
calendar.get(Calendar.YEAR).toString()
)
binding.orderDetailsShippingAddLayout.shipAddValueTv.text =
getCompleteAddress(orderData.deliveryAddress)
binding.orderDetailsShippingAddLayout.shipCurrStatusValueTv.text = orderData.status
setPriceCard(orderData)
}
private fun setPriceCard(orderData: UserData.OrderItem) {
binding.orderDetailsPaymentLayout.priceItemsLabelTv.text = getString(
R.string.price_card_items_string,
getItemsCount(orderData.items).toString()
)
val itemsPriceTotal = getItemsPriceTotal(orderData.itemsPrices, orderData.items)
binding.orderDetailsPaymentLayout.priceItemsAmountTv.text =
getString(
R.string.price_text,
itemsPriceTotal.toString()
)
binding.orderDetailsPaymentLayout.priceShippingAmountTv.text =
getString(R.string.price_text, "0")
binding.orderDetailsPaymentLayout.priceChargesAmountTv.text =
getString(R.string.price_text, "0")
binding.orderDetailsPaymentLayout.priceTotalAmountTv.text =
getString(R.string.price_text, (itemsPriceTotal + orderData.shippingCharges).toString())
}
private fun setProductsAdapter(itemsList: List?) {
val items = itemsList ?: emptyList()
val likesList = viewModel.userLikes.value ?: emptyList()
val proList = viewModel.orderProducts.value ?: emptyList()
productsAdapter = OrderProductsAdapter(requireContext(), items, proList, likesList)
}
private fun showDialogWithItems(checkedOption: Int = 0, orderId: String) {
val categoryItems: Array = OrderStatus.values().map { it.name }.toTypedArray()
var checkedItem = checkedOption
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.status_dialog_title))
.setSingleChoiceItems(categoryItems, checkedItem) { _, which ->
checkedItem = which
}
.setNegativeButton(getString(R.string.pro_cat_dialog_cancel_btn)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(getString(R.string.pro_cat_dialog_ok_btn)) { dialog, _ ->
if (checkedItem == -1) {
dialog.cancel()
} else {
viewModel.onSetStatusOfOrder(orderId, categoryItems[checkedItem])
}
dialog.cancel()
}
.show()
}
}
private fun getItemsCount(cartItems: List): Int {
var totalCount = 0
cartItems.forEach {
totalCount += it.quantity
}
return totalCount
}
private fun getItemsPriceTotal(
priceList: Map,
cartItems: List
): Double {
var totalPrice = 0.0
priceList.forEach { (itemId, price) ->
totalPrice += price * (cartItems.find { it.itemId == itemId }?.quantity ?: 1)
}
return totalPrice
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrderProductsAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.Product
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.databinding.CartListItemBinding
class OrderProductsAdapter(
private val context: Context, items: List,
products: List, userLikes: List
) : RecyclerView.Adapter() {
var data: List = items
var proList: List = products
var likesList: List = userLikes
inner class ViewHolder(private val binding: CartListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(itemData: UserData.CartItem) {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
val proData = proList.find { it.productId == itemData.productId } ?: Product()
binding.cartProductTitleTv.text = proData.name
binding.cartProductPriceTv.text =
context.getString(R.string.price_text, proData.price.toString())
if (proData.images.isNotEmpty()) {
val imgUrl = proData.images[0].toUri().buildUpon().scheme("https").build()
Glide.with(context)
.asBitmap()
.load(imgUrl)
.into(binding.productImageView)
binding.productImageView.clipToOutline = true
}
binding.cartProductDeleteBtn.visibility = View.GONE
binding.cartProductQuantityButtonsLayout.visibility = View.GONE
binding.cartProductLikeBtn.visibility = View.GONE
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
CartListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount() = data.size
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrderSuccessFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentOrderSuccessBinding
import com.vishalgaur.shoppingapp.viewModels.OrderViewModel
class OrderSuccessFragment : Fragment() {
private lateinit var binding: FragmentOrderSuccessBinding
private val orderViewModel: OrderViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentOrderSuccessBinding.inflate(layoutInflater)
binding.loaderLayout.loaderCard.visibility = View.VISIBLE
binding.loaderLayout.loadingMessage.text = getString(R.string.process_order_msg)
binding.loaderLayout.circularLoader.showAnimationBehavior
binding.orderConstraintGroup.visibility = View.GONE
setObservers()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.backToHomeBtn.setOnClickListener {
findNavController().navigate(R.id.action_orderSuccessFragment_to_homeFragment)
}
}
private fun setObservers() {
orderViewModel.orderStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.loaderLayout.loaderCard.visibility = View.VISIBLE
}
else -> {
binding.orderConstraintGroup.visibility = View.VISIBLE
binding.loaderLayout.loaderCard.visibility = View.GONE
binding.redirectHomeTimerTv.text =
getString(R.string.redirect_home_timer_text, "5")
countDownTimer.start()
}
}
}
}
private val countDownTimer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val sec = millisUntilFinished / 1000
binding.redirectHomeTimerTv.text =
getString(R.string.redirect_home_timer_text, sec.toString())
}
override fun onFinish() {
findNavController().navigate(R.id.action_orderSuccessFragment_to_homeFragment)
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrdersAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.UserData
import com.vishalgaur.shoppingapp.databinding.LayoutOrderSummaryCardBinding
import java.time.Month
import java.util.*
class OrdersAdapter(ordersList: List, private val context: Context) :
RecyclerView.Adapter() {
lateinit var onClickListener: OnClickListener
var data: List = ordersList
inner class ViewHolder(private val binding: LayoutOrderSummaryCardBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(orderData: UserData.OrderItem) {
binding.orderSummaryCard.setOnClickListener { onClickListener.onCardClick(orderData.orderId) }
binding.orderSummaryIdTv.text = orderData.orderId
val calendar = Calendar.getInstance()
calendar.time = orderData.orderDate
binding.orderSummaryDateTv.text =
context.getString(
R.string.order_date_text,
Month.values()[(calendar.get(Calendar.MONTH))].name,
calendar.get(Calendar.DAY_OF_MONTH).toString(),
calendar.get(Calendar.YEAR).toString()
)
binding.orderSummaryStatusValueTv.text = orderData.status
val totalItems = orderData.items.map { it.quantity }.sum()
binding.orderSummaryItemsCountTv.text =
context.getString(R.string.order_items_count_text, totalItems.toString())
var totalAmount = 0.0
orderData.itemsPrices.forEach { (itemId, price) ->
totalAmount += price * (orderData.items.find { it.itemId == itemId }?.quantity ?: 1)
}
binding.orderSummaryTotalAmountTv.text =
context.getString(R.string.price_text, totalAmount.toString())
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutOrderSummaryCardBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount() = data.size
interface OnClickListener {
fun onCardClick(orderId: String)
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/OrdersFragment.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.data.utils.StoreDataStatus
import com.vishalgaur.shoppingapp.databinding.FragmentOrdersBinding
import com.vishalgaur.shoppingapp.viewModels.HomeViewModel
private const val TAG = "OrdersFragment"
class OrdersFragment : Fragment() {
private lateinit var binding: FragmentOrdersBinding
private lateinit var ordersAdapter: OrdersAdapter
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentOrdersBinding.inflate(layoutInflater)
setViews()
setObservers()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getAllOrders()
}
private fun setViews() {
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.ordersAppBar.topAppBar.title = getString(R.string.orders_fragment_title)
binding.ordersAppBar.topAppBar.setNavigationOnClickListener {
findNavController().navigateUp()
}
binding.ordersEmptyTextView.visibility = View.GONE
if (context != null) {
ordersAdapter = OrdersAdapter(emptyList(), requireContext())
ordersAdapter.onClickListener = object : OrdersAdapter.OnClickListener {
override fun onCardClick(orderId: String) {
Log.d(TAG, "onOrderSummaryClick: Getting order details")
findNavController().navigate(
R.id.action_ordersFragment_to_orderDetailsFragment,
bundleOf("orderId" to orderId)
)
}
}
binding.orderAllOrdersRecyclerView.adapter = ordersAdapter
}
}
private fun setObservers() {
viewModel.storeDataStatus.observe(viewLifecycleOwner) { status ->
when (status) {
StoreDataStatus.LOADING -> {
binding.orderAllOrdersRecyclerView.visibility = View.GONE
binding.ordersEmptyTextView.visibility = View.GONE
binding.loaderLayout.loaderFrameLayout.visibility = View.VISIBLE
binding.loaderLayout.circularLoader.showAnimationBehavior
}
else -> {
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
}
}
if (status != null && status != StoreDataStatus.LOADING) {
viewModel.userOrders.observe(viewLifecycleOwner) { orders ->
if (orders.isNotEmpty()) {
ordersAdapter.data = orders.sortedByDescending { it.orderDate }
binding.orderAllOrdersRecyclerView.adapter?.notifyDataSetChanged()
binding.orderAllOrdersRecyclerView.visibility = View.VISIBLE
} else if (orders.isEmpty()) {
binding.loaderLayout.circularLoader.hideAnimationBehavior
binding.loaderLayout.loaderFrameLayout.visibility = View.GONE
binding.ordersEmptyTextView.visibility = View.VISIBLE
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/vishalgaur/shoppingapp/ui/home/PayByAdapter.kt
================================================
package com.vishalgaur.shoppingapp.ui.home
import android.annotation.SuppressLint
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.vishalgaur.shoppingapp.R
import com.vishalgaur.shoppingapp.databinding.LayoutListItemBinding
private const val TAG = "PayByAdapter"
class PayByAdapter(private val data: List