diff --git a/Nexaas/.gitignore b/Nexaas/.gitignore new file mode 100644 index 00000000..870cb17b --- /dev/null +++ b/Nexaas/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +gradlew +gradlew.bat \ No newline at end of file diff --git a/Nexaas/README.md b/Nexaas/README.md new file mode 100644 index 00000000..8b193b55 --- /dev/null +++ b/Nexaas/README.md @@ -0,0 +1,35 @@ +# Test Android Developer [Nexaas](https://nexaas.com/) + + +### Code Features +---------- +- [Kotlin](https://kotlinlang.org/) +- [Coroutines](https://developer.android.com/kotlin/coroutines) +- Dependency injection in multi modules with [Koin](https://insert-koin.io/) +- [Retrofit](https://square.github.io/retrofit/) + [Gson](https://github.com/google/gson). +- [MVVM](https://developer.android.com/jetpack/guide) +- [Android Architecture Components](https://developer.android.com/topic/libraries/architecture) + - [Data Binding](https://developer.android.com/topic/libraries/data-binding) + - [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) + - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) + - [Navigation](https://developer.android.com/topic/libraries/architecture/navigation) + - [Room](https://developer.android.com/topic/libraries/architecture/room) +- Unit Testing with [JUnit4](https://github.com/junit-team/junit4), [Mockito](https://site.mockito.org/) and [Hamcrest](http://hamcrest.org/JavaHamcrest/tutorial) +- Load images with [Glide](https://github.com/bumptech/glide) + +### App Modules +---------- +- app +- common +- network +- data +- domain +- storage + +### Screens +---------- + + +### Author +---------- +Ricarlo Silva - [LinkedIn](https://www.linkedin.com/in/ricarlo-silva/) \ No newline at end of file diff --git a/Nexaas/app/.gitignore b/Nexaas/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Nexaas/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Nexaas/app/build.gradle b/Nexaas/app/build.gradle new file mode 100644 index 00000000..f11cac20 --- /dev/null +++ b/Nexaas/app/build.gradle @@ -0,0 +1,60 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: 'androidx.navigation.safeargs.kotlin' +apply from: "$rootDir/flavors.gradle" + +android { + compileSdkVersion versions.compileSdkVersion + buildToolsVersion versions.buildToolsVersion + + defaultConfig { + applicationId "br.com.nexaas" + minSdkVersion versions.minSdkVersion + targetSdkVersion versions.targetSdkVersion + versionCode versions.versionCode + versionName versions.versionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + dataBinding = true + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + + implementation(project(":common")) + implementation(project(":data")) + implementation(project(":domain")) + implementation(project(":network")) + + implementation deps.androidx.lifecycle.livedata + implementation deps.androidx.lifecycle.viewmodel + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + // Navigation + implementation deps.androidx.navigation.fragment + implementation deps.androidx.navigation.ui + + // Test + testImplementation deps.junit + testImplementation deps.mockito.core + testImplementation deps.hamcrest.library + testImplementation deps.koin.test + testImplementation deps.coroutines.test + testImplementation deps.androidx.test.arch + androidTestImplementation deps.androidx.test.junit + androidTestImplementation deps.androidx.test.espresso + +} \ No newline at end of file diff --git a/Nexaas/app/proguard-rules.pro b/Nexaas/app/proguard-rules.pro new file mode 100644 index 00000000..33bda4f4 --- /dev/null +++ b/Nexaas/app/proguard-rules.pro @@ -0,0 +1,40 @@ +# 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 + + +#### Coroutines #### +# ServiceLoader support +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} +-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} + +# Most of volatile fields are updated with AFU and should not be mangled +-keepclassmembernames class kotlinx.* { + volatile ; +} + +# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater +-keepclassmembernames class kotlin.coroutines.SafeContinuation { + volatile ; +} +################### \ No newline at end of file diff --git a/Nexaas/app/src/androidTest/java/br/com/nexaas/ExampleInstrumentedTest.kt b/Nexaas/app/src/androidTest/java/br/com/nexaas/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..bea281cd --- /dev/null +++ b/Nexaas/app/src/androidTest/java/br/com/nexaas/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package br.com.nexaas + +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("br.com.nexaas", appContext.packageName) + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/AndroidManifest.xml b/Nexaas/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..85394f71 --- /dev/null +++ b/Nexaas/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/App.kt b/Nexaas/app/src/main/java/br/com/nexaas/App.kt new file mode 100644 index 00000000..a27cf465 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/App.kt @@ -0,0 +1,33 @@ +package br.com.nexaas + +import android.app.Application +import br.com.nexaas.common.di.CommonModule +import br.com.nexaas.data.di.DataModule +import br.com.nexaas.di.PresentationModule +import br.com.nexaas.domain.di.DomainModule +import br.com.nexaas.network.di.NetworkModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class App : Application() { + + override fun onCreate() { + super.onCreate() + setupDI() + } + + private fun setupDI() { + startKoin { + androidContext(this@App) + modules( + listOf( + NetworkModule.module, + CommonModule.module, + DataModule.module, + DomainModule.module, + PresentationModule.module + ) + ) + } + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/di/PresentationModule.kt b/Nexaas/app/src/main/java/br/com/nexaas/di/PresentationModule.kt new file mode 100644 index 00000000..1df3febd --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/di/PresentationModule.kt @@ -0,0 +1,18 @@ +package br.com.nexaas.di + +import br.com.nexaas.features.cart.CartViewModel +import br.com.nexaas.features.product.ProductDetailsViewModel +import org.koin.android.viewmodel.dsl.viewModel +import org.koin.dsl.module + +object PresentationModule { + val module = module { + viewModel { + CartViewModel(getCartUseCase = get(), dispatcher = get()) + } + + viewModel { + ProductDetailsViewModel() + } + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartAdapter.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartAdapter.kt new file mode 100644 index 00000000..eb662134 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartAdapter.kt @@ -0,0 +1,39 @@ +package br.com.nexaas.features.cart + +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.DiffUtil +import br.com.nexaas.BR +import br.com.nexaas.R +import br.com.nexaas.common.ui.base.adapter.BaseAdapter +import br.com.nexaas.common.ui.base.adapter.BaseViewHolder +import br.com.nexaas.features.cart.data.entity.CartItemVO + +class CartAdapter(private val listener: OnClickListener) : BaseAdapter(DiffCallback) { + + object DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: CartItemVO, newItem: CartItemVO): Boolean { + return oldItem.name == newItem.name + } + + override fun areContentsTheSame(oldItem: CartItemVO, newItem: CartItemVO): Boolean { + return oldItem == newItem + } + } + + inner class ViewHolder(private val binding: ViewDataBinding, private val listener: OnClickListener) : BaseViewHolder(binding) { + override fun onBind(item: CartItemVO, position: Int) { + binding.setVariable(BR.item, item) + binding.setVariable(BR.listener, listener) + binding.executePendingBindings() + } + } + + override fun instantiateViewHolder(view: ViewDataBinding, viewType: Int): ViewHolder { + return ViewHolder(view, listener) + } + + override fun getItemView(viewType: Int): Int { + return R.layout.item_list_cart + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartFragment.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartFragment.kt new file mode 100644 index 00000000..ab08b8b6 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartFragment.kt @@ -0,0 +1,75 @@ +package br.com.nexaas.features.cart + +import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import br.com.nexaas.R +import br.com.nexaas.common.ui.base.BaseFragment +import br.com.nexaas.common.ui.base.ViewState +import br.com.nexaas.databinding.FragmentCartBinding +import br.com.nexaas.features.cart.data.entity.CartItemVO +import br.com.nexaas.features.cart.data.mapper.CartItemToProductMapper +import com.google.android.material.snackbar.Snackbar +import org.koin.android.viewmodel.ext.android.viewModel +import java.io.IOException + +class CartFragment : BaseFragment(), OnClickListener { + + private val viewModel: CartViewModel by viewModel() + + private val cartAdapter by lazy { CartAdapter(this) } + + override fun getLayoutRes(): Int { + return R.layout.fragment_cart + } + + override fun initView(savedInstanceState: Bundle?) { + binding.apply { + this.viewmodel = viewModel + } + binding.rvItemsCart.apply { + adapter = cartAdapter + addItemDecoration( + DividerItemDecoration( + context, + DividerItemDecoration.VERTICAL + ) + ) + } + subscribeUi() + } + + private fun subscribeUi() { + viewModel.items.observe(viewLifecycleOwner, Observer { + when (it) { + is ViewState.Success -> { + cartAdapter.submitList(it.data.items) + } + is ViewState.Error -> { + val message = when (it.error) { + is IOException -> { + getString(R.string.network_error) + } + else -> { + getString(R.string.generic_error) + } + } + Snackbar.make(binding.root, message, Snackbar.LENGTH_INDEFINITE) + .setAction(getString(R.string.action_try_again)) { _ -> + it.retry?.invoke() + }.show() + } + is ViewState.Loading -> { + } + } + }) + } + + override fun onClickProduct(itemVO: CartItemVO) { + val product = CartItemToProductMapper().transform(itemVO) + + val destinations = CartFragmentDirections.navigateToProductDetails(product) + findNavController().navigate(destinations) + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartViewModel.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartViewModel.kt new file mode 100644 index 00000000..9b72e694 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/CartViewModel.kt @@ -0,0 +1,67 @@ +package br.com.nexaas.features.cart + +import androidx.lifecycle.* +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import br.com.nexaas.common.ui.base.ViewState +import br.com.nexaas.domain.usecase.cart.IGetCartUseCase +import br.com.nexaas.features.cart.data.entity.CartVO +import br.com.nexaas.features.cart.data.mapper.CartItemVOMapper +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +class CartViewModel( + private val getCartUseCase: IGetCartUseCase, + private val dispatcher: ICoroutinesDispatcherProvider +) : ViewModel() { + + private val _items = MutableLiveData>() + private val _itemsCount = MutableLiveData(0) + private val _total = MutableLiveData(0L) + private val _subtotal = MutableLiveData(0L) + private val _shipping = MutableLiveData(0L) + private val _tax = MutableLiveData(0L) + + val items: LiveData> = _items + + fun itemsCount() = _itemsCount + + fun total() = _total + + fun subtotal() = _subtotal + + fun shipping() = _shipping + + fun tax() = _tax + + fun loading() = Transformations.map(_items) { + (it is ViewState.Loading) + } + + init { + loadCart() + } + + fun loadCart() { + viewModelScope.launch(dispatcher.ui()) { + try { + _items.value = ViewState.Loading() + + getCartUseCase.execute().collect { cartItems -> + + val cartVO = CartVO(CartItemVOMapper().transform(cartItems)) + _items.value = ViewState.Success(cartVO) + + _itemsCount.value = cartVO.itemsCount() + _total.value = cartVO.total() + _subtotal.value = cartVO.subtotal() + _shipping.value = cartVO.shipping() + _tax.value = cartVO.tax() + } + + } catch (e: Exception) { + _items.value = ViewState.Error(e) { loadCart() } + } + } + } + +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/OnClickListener.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/OnClickListener.kt new file mode 100644 index 00000000..714d7d66 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/OnClickListener.kt @@ -0,0 +1,7 @@ +package br.com.nexaas.features.cart + +import br.com.nexaas.features.cart.data.entity.CartItemVO + +interface OnClickListener { + fun onClickProduct(itemVO: CartItemVO) +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/entity/CartItemVO.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/entity/CartItemVO.kt new file mode 100644 index 00000000..f1ca3c0c --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/entity/CartItemVO.kt @@ -0,0 +1,18 @@ +package br.com.nexaas.features.cart.data.entity + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.android.parcel.Parcelize + +@Keep +@Parcelize +data class CartItemVO( + val quantity: Int, + val shipping: Long, + val imageUrl: String, + val price: Long, + val name: String, + val description: String, + val tax: Long, + val stock: Int +) : Parcelable \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/entity/CartVO.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/entity/CartVO.kt new file mode 100644 index 00000000..857b405c --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/entity/CartVO.kt @@ -0,0 +1,22 @@ +package br.com.nexaas.features.cart.data.entity + +import android.os.Parcelable +import androidx.annotation.Keep +import br.com.nexaas.common.utils.sumByLong +import kotlinx.android.parcel.Parcelize + +@Keep +@Parcelize +data class CartVO( + val items :List +) : Parcelable { + fun itemsCount() = items.count() + + fun total() = subtotal() + shipping() + tax() + + fun subtotal() = items.sumByLong { it.price * it.quantity } + + fun shipping() = items.sumByLong { it.shipping } + + fun tax() = items.sumByLong { it.tax } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/mapper/CartItemToProductMapper.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/mapper/CartItemToProductMapper.kt new file mode 100644 index 00000000..ee1aa477 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/mapper/CartItemToProductMapper.kt @@ -0,0 +1,22 @@ +package br.com.nexaas.features.cart.data.mapper + +import br.com.nexaas.common.utils.Mapper +import br.com.nexaas.features.cart.data.entity.CartItemVO +import br.com.nexaas.features.product.data.ProductVO + +class CartItemToProductMapper : Mapper { + override fun transform(from: CartItemVO): ProductVO { + return ProductVO( + name = from.name, + stock = from.stock, + imageUrl = from.imageUrl, + price = from.price, + description = from.description + ) + } + + override fun transform(from: List): List { + return from.map { transform(it) } + } + +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/mapper/CartItemVOMapper.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/mapper/CartItemVOMapper.kt new file mode 100644 index 00000000..f2351d43 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/cart/data/mapper/CartItemVOMapper.kt @@ -0,0 +1,25 @@ +package br.com.nexaas.features.cart.data.mapper + +import br.com.nexaas.common.utils.Mapper +import br.com.nexaas.domain.entity.CartModel +import br.com.nexaas.features.cart.data.entity.CartItemVO + +class CartItemVOMapper : Mapper { + override fun transform(from: CartModel): CartItemVO { + return CartItemVO( + quantity = from.quantity, + shipping = from.shipping, + imageUrl = from.imageUrl, + price = from.price, + name = from.name, + description = from.description, + tax = from.tax, + stock = from.stock + ) + } + + override fun transform(from: List): List { + return from.map { transform(it) } + } + +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/main/MainActivity.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/main/MainActivity.kt new file mode 100644 index 00000000..a8bcf321 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/main/MainActivity.kt @@ -0,0 +1,12 @@ +package br.com.nexaas.features.main + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import br.com.nexaas.R + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/product/ProductDetailsActivity.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/product/ProductDetailsActivity.kt new file mode 100644 index 00000000..4c4e2e18 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/product/ProductDetailsActivity.kt @@ -0,0 +1,40 @@ +package br.com.nexaas.features.product + +import android.os.Bundle +import android.view.MenuItem +import androidx.navigation.navArgs +import br.com.nexaas.R +import br.com.nexaas.common.ui.base.BaseActivity +import br.com.nexaas.databinding.ActivityProductDetailsBinding +import org.koin.android.viewmodel.ext.android.viewModel + +class ProductDetailsActivity : BaseActivity() { + + private val viewModel: ProductDetailsViewModel by viewModel() + + private val args by navArgs() + + override fun getLayoutRes(): Int { + return R.layout.activity_product_details + } + + override fun initView(savedInstanceState: Bundle?) { + binding.apply { + this.viewmodel = viewModel + product = args.product + } + binding.toolbar.setNavigationOnClickListener { + finish() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.app_bar_search -> { + // TODO + true + } + else -> super.onOptionsItemSelected(item) + } + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/product/ProductDetailsViewModel.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/product/ProductDetailsViewModel.kt new file mode 100644 index 00000000..70352cdc --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/product/ProductDetailsViewModel.kt @@ -0,0 +1,10 @@ +package br.com.nexaas.features.product + +import androidx.lifecycle.ViewModel + +class ProductDetailsViewModel : ViewModel() { + + fun clickRemoveFromCart() { + // TODO + } +} \ No newline at end of file diff --git a/Nexaas/app/src/main/java/br/com/nexaas/features/product/data/ProductVO.kt b/Nexaas/app/src/main/java/br/com/nexaas/features/product/data/ProductVO.kt new file mode 100644 index 00000000..0a790dc9 --- /dev/null +++ b/Nexaas/app/src/main/java/br/com/nexaas/features/product/data/ProductVO.kt @@ -0,0 +1,15 @@ +package br.com.nexaas.features.product.data + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.android.parcel.Parcelize + +@Keep +@Parcelize +data class ProductVO( + val imageUrl: String, + val price: Long, + val name: String, + val description: String, + val stock: Int +) : Parcelable \ No newline at end of file diff --git a/Nexaas/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Nexaas/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/Nexaas/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/drawable/ic_baseline_close_24.xml b/Nexaas/app/src/main/res/drawable/ic_baseline_close_24.xml new file mode 100644 index 00000000..16d6d37d --- /dev/null +++ b/Nexaas/app/src/main/res/drawable/ic_baseline_close_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/Nexaas/app/src/main/res/drawable/ic_launcher_background.xml b/Nexaas/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/Nexaas/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nexaas/app/src/main/res/drawable/ic_search_black_24dp.xml b/Nexaas/app/src/main/res/drawable/ic_search_black_24dp.xml new file mode 100644 index 00000000..c572778e --- /dev/null +++ b/Nexaas/app/src/main/res/drawable/ic_search_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Nexaas/app/src/main/res/layout/activity_main.xml b/Nexaas/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..581c4144 --- /dev/null +++ b/Nexaas/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/layout/activity_product_details.xml b/Nexaas/app/src/main/res/layout/activity_product_details.xml new file mode 100644 index 00000000..7736343c --- /dev/null +++ b/Nexaas/app/src/main/res/layout/activity_product_details.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/layout/fragment_cart.xml b/Nexaas/app/src/main/res/layout/fragment_cart.xml new file mode 100644 index 00000000..66d95cad --- /dev/null +++ b/Nexaas/app/src/main/res/layout/fragment_cart.xml @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/layout/item_list_cart.xml b/Nexaas/app/src/main/res/layout/item_list_cart.xml new file mode 100644 index 00000000..9a2e4a8c --- /dev/null +++ b/Nexaas/app/src/main/res/layout/item_list_cart.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/menu/menu_cart.xml b/Nexaas/app/src/main/res/menu/menu_cart.xml new file mode 100644 index 00000000..1d216170 --- /dev/null +++ b/Nexaas/app/src/main/res/menu/menu_cart.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/menu/menu_product_details.xml b/Nexaas/app/src/main/res/menu/menu_product_details.xml new file mode 100644 index 00000000..8b67cb80 --- /dev/null +++ b/Nexaas/app/src/main/res/menu/menu_product_details.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Nexaas/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/Nexaas/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Nexaas/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/Nexaas/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Nexaas/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..a571e600 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Nexaas/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Nexaas/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..61da551c Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/Nexaas/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Nexaas/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c41dd285 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Nexaas/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Nexaas/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..db5080a7 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/Nexaas/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Nexaas/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..6dba46da Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Nexaas/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Nexaas/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..da31a871 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/Nexaas/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Nexaas/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..15ac6817 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Nexaas/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Nexaas/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..b216f2d3 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/Nexaas/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Nexaas/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..f25a4197 Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Nexaas/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Nexaas/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..e96783cc Binary files /dev/null and b/Nexaas/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/Nexaas/app/src/main/res/navigation/nav_graph.xml b/Nexaas/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..642aa450 --- /dev/null +++ b/Nexaas/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/values/colors.xml b/Nexaas/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..e45cde42 --- /dev/null +++ b/Nexaas/app/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + + #FF000000 + #FFFFFFFF + #FFAAAAAA + #FFDDDDDD + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/values/dimens.xml b/Nexaas/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..214b212e --- /dev/null +++ b/Nexaas/app/src/main/res/values/dimens.xml @@ -0,0 +1,14 @@ + + + 24dp + 16dp + 8dp + 4dp + + 56dp + + 48dp + + + 18sp + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/values/strings.xml b/Nexaas/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..c6fd0366 --- /dev/null +++ b/Nexaas/app/src/main/res/values/strings.xml @@ -0,0 +1,33 @@ + + Nexaas + + Error… Check your connection! + Error… + Try again + + + Cart + Tax + Shipping + Subtotal + Total + Checkout + + + + only 1 left in stock + in stock + + + + + %d Item in your cart + %d Items in your cart + + + + Product details + Remove from cart + Product image + + \ No newline at end of file diff --git a/Nexaas/app/src/main/res/values/styles.xml b/Nexaas/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..4f40f021 --- /dev/null +++ b/Nexaas/app/src/main/res/values/styles.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nexaas/app/src/test/java/br/com/nexaas/features/cart/CartViewModelTest.kt b/Nexaas/app/src/test/java/br/com/nexaas/features/cart/CartViewModelTest.kt new file mode 100644 index 00000000..3b93cd59 --- /dev/null +++ b/Nexaas/app/src/test/java/br/com/nexaas/features/cart/CartViewModelTest.kt @@ -0,0 +1,95 @@ +package br.com.nexaas.features.cart + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import br.com.nexaas.data.di.DataModule +import br.com.nexaas.di.PresentationModule +import br.com.nexaas.domain.di.DomainModule +import br.com.nexaas.domain.usecase.cart.IGetCartUseCase +import br.com.nexaas.utils.CoroutinesDispatcherProviderTest +import br.com.nexaas.utils.createCartModel +import br.com.nexaas.utils.getOrAwaitValue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.context.loadKoinModules +import org.koin.core.context.startKoin +import org.koin.dsl.module +import org.koin.test.AutoCloseKoinTest +import org.koin.test.inject +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class CartViewModelTest : AutoCloseKoinTest() { + + private val viewModel: CartViewModel by inject() + + // Executes tasks in the Architecture Components in the same thread + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @Mock + private lateinit var mockGetCartUseCase: IGetCartUseCase + + + @Before + fun setup() { + startKoin { + modules( + DataModule.module, + DomainModule.module, + PresentationModule.module + ) + } + + loadKoinModules( + module(override = true) { + single { + mockGetCartUseCase + } + + single { + CoroutinesDispatcherProviderTest() + } + } + ) + } + + @Test + fun load_cart() = runBlocking { + // Given + val list = listOf( + createCartModel(), + createCartModel() + ) + val flow = flow { + emit(list) + } + `when`(mockGetCartUseCase.execute()).thenReturn(flow) + + // When load cart + viewModel.loadCart() + + val itemsCount = viewModel.itemsCount().getOrAwaitValue() + val total = viewModel.total().getOrAwaitValue() + val subtotal = viewModel.subtotal().getOrAwaitValue() + val shipping = viewModel.shipping().getOrAwaitValue() + val tax = viewModel.tax().getOrAwaitValue() + + // Then + assertEquals(2, itemsCount) + assertEquals(0L, total) + assertEquals(0L, subtotal) + assertEquals(0L, shipping) + assertEquals(0L, tax) + } + +} \ No newline at end of file diff --git a/Nexaas/app/src/test/java/br/com/nexaas/utils/CoroutinesDispatcherProviderTest.kt b/Nexaas/app/src/test/java/br/com/nexaas/utils/CoroutinesDispatcherProviderTest.kt new file mode 100644 index 00000000..585e78ef --- /dev/null +++ b/Nexaas/app/src/test/java/br/com/nexaas/utils/CoroutinesDispatcherProviderTest.kt @@ -0,0 +1,25 @@ +package br.com.nexaas.utils + +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher + +@ExperimentalCoroutinesApi +class CoroutinesDispatcherProviderTest( + private val dispatcher : TestCoroutineDispatcher = TestCoroutineDispatcher() +) : ICoroutinesDispatcherProvider { + + override fun io(): CoroutineDispatcher { + return dispatcher + } + + override fun ui(): CoroutineDispatcher { + return dispatcher + } + + override fun computation(): CoroutineDispatcher { + return dispatcher + } + +} \ No newline at end of file diff --git a/Nexaas/app/src/test/java/br/com/nexaas/utils/LiveDataTestUtil.kt b/Nexaas/app/src/test/java/br/com/nexaas/utils/LiveDataTestUtil.kt new file mode 100644 index 00000000..243fe4bb --- /dev/null +++ b/Nexaas/app/src/test/java/br/com/nexaas/utils/LiveDataTestUtil.kt @@ -0,0 +1,35 @@ +package br.com.nexaas.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} +): 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) + + afterObserve.invoke() + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + this.removeObserver(observer) + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} \ No newline at end of file diff --git a/Nexaas/app/src/test/java/br/com/nexaas/utils/TestData.kt b/Nexaas/app/src/test/java/br/com/nexaas/utils/TestData.kt new file mode 100644 index 00000000..828f213c --- /dev/null +++ b/Nexaas/app/src/test/java/br/com/nexaas/utils/TestData.kt @@ -0,0 +1,23 @@ +package br.com.nexaas.utils + +import br.com.nexaas.domain.entity.CartModel + +fun createCartModel( + quantity: Int = 0, + shipping: Long = 0L, + imageUrl: String = "", + price: Long = 0L, + name: String = "", + description: String = "", + tax: Long = 0L, + stock: Int = 0 +) = CartModel( + quantity = quantity, + shipping = shipping, + imageUrl = imageUrl, + price = price, + name = name, + description = description, + tax = tax, + stock = stock +) \ No newline at end of file diff --git a/Nexaas/build.gradle b/Nexaas/build.gradle new file mode 100644 index 00000000..ed47c756 --- /dev/null +++ b/Nexaas/build.gradle @@ -0,0 +1,25 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + apply from: 'dependencies.gradle' + + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.0.0" + classpath deps.kotlin.plugin + classpath deps.androidx.navigation.plugin + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/Nexaas/common/.gitignore b/Nexaas/common/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Nexaas/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Nexaas/common/build.gradle b/Nexaas/common/build.gradle new file mode 100644 index 00000000..f690eede --- /dev/null +++ b/Nexaas/common/build.gradle @@ -0,0 +1,63 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply from: "$rootDir/flavors.gradle" + +android { + compileSdkVersion versions.compileSdkVersion + buildToolsVersion versions.buildToolsVersion + + defaultConfig { + minSdkVersion versions.minSdkVersion + targetSdkVersion versions.targetSdkVersion + versionCode versions.versionCode + versionName versions.versionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + dataBinding = true + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + // Kotlin + api deps.kotlin.jdk + + // DI + api deps.koin.android + api deps.koin.viewmodel + + api deps.androidx.annotation + + // Coroutines + api deps.coroutines.core + api deps.coroutines.android + + // UI + api deps.androidx.core + api deps.androidx.appcompat + api deps.androidx.constraintlayout + api deps.google.material + + // IMAGE + implementation deps.glide.core + kapt deps.glide.compiler + + // Test + testImplementation deps.junit + androidTestImplementation deps.androidx.test.junit + androidTestImplementation deps.androidx.test.espresso + +} \ No newline at end of file diff --git a/Nexaas/common/consumer-rules.pro b/Nexaas/common/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Nexaas/common/proguard-rules.pro b/Nexaas/common/proguard-rules.pro new file mode 100644 index 00000000..7309a5a5 --- /dev/null +++ b/Nexaas/common/proguard-rules.pro @@ -0,0 +1,32 @@ +# 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 + +########### Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep class * extends com.bumptech.glide.module.AppGlideModule { + (...); +} +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} +############# \ No newline at end of file diff --git a/Nexaas/common/src/androidTest/java/br/com/nexaas/common/ExampleInstrumentedTest.kt b/Nexaas/common/src/androidTest/java/br/com/nexaas/common/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..5dc3f6c3 --- /dev/null +++ b/Nexaas/common/src/androidTest/java/br/com/nexaas/common/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package br.com.nexaas.common + +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("br.com.nexaas.common.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/Nexaas/common/src/main/AndroidManifest.xml b/Nexaas/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a9d5ee6a --- /dev/null +++ b/Nexaas/common/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/coroutines/CoroutinesDispatcherProvider.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/coroutines/CoroutinesDispatcherProvider.kt new file mode 100644 index 00000000..a2891d06 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/coroutines/CoroutinesDispatcherProvider.kt @@ -0,0 +1,19 @@ +package br.com.nexaas.common.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +class CoroutinesDispatcherProvider : ICoroutinesDispatcherProvider { + override fun io(): CoroutineDispatcher { + return Dispatchers.IO + } + + override fun ui(): CoroutineDispatcher { + return Dispatchers.Main + } + + override fun computation(): CoroutineDispatcher { + return Dispatchers.Default + } + +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/coroutines/ICoroutinesDispatcherProvider.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/coroutines/ICoroutinesDispatcherProvider.kt new file mode 100644 index 00000000..8b12325d --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/coroutines/ICoroutinesDispatcherProvider.kt @@ -0,0 +1,9 @@ +package br.com.nexaas.common.coroutines + +import kotlinx.coroutines.CoroutineDispatcher + +interface ICoroutinesDispatcherProvider { + fun io(): CoroutineDispatcher + fun ui(): CoroutineDispatcher + fun computation(): CoroutineDispatcher +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/di/CommonModule.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/di/CommonModule.kt new file mode 100644 index 00000000..f30bd937 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/di/CommonModule.kt @@ -0,0 +1,13 @@ +package br.com.nexaas.common.di + +import br.com.nexaas.common.coroutines.CoroutinesDispatcherProvider +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import org.koin.dsl.module + +object CommonModule { + val module = module { + single { + CoroutinesDispatcherProvider() + } + } +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/BaseActivity.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/BaseActivity.kt new file mode 100644 index 00000000..55741a7d --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/BaseActivity.kt @@ -0,0 +1,27 @@ +package br.com.nexaas.common.ui.base + +import android.os.Bundle +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil.setContentView +import androidx.databinding.ViewDataBinding + +abstract class BaseActivity : AppCompatActivity() { + + @LayoutRes + abstract fun getLayoutRes(): Int + + abstract fun initView(savedInstanceState: Bundle?) + + protected val binding: T by lazy { + setContentView(this, getLayoutRes()) + .apply { + lifecycleOwner = this@BaseActivity + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initView(savedInstanceState = savedInstanceState) + } +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/BaseFragment.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/BaseFragment.kt new file mode 100644 index 00000000..96696549 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/BaseFragment.kt @@ -0,0 +1,36 @@ +package br.com.nexaas.common.ui.base + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment + +abstract class BaseFragment : Fragment() { + + @LayoutRes + abstract fun getLayoutRes(): Int + + abstract fun initView(savedInstanceState: Bundle?) + + protected lateinit var binding: T + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, false) + return binding.apply { + lifecycleOwner = viewLifecycleOwner + }.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + initView(savedInstanceState = savedInstanceState) + } +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/ViewState.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/ViewState.kt new file mode 100644 index 00000000..12c056d8 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/ViewState.kt @@ -0,0 +1,7 @@ +package br.com.nexaas.common.ui.base + +sealed class ViewState { + class Success(val data: T) : ViewState() + class Loading : ViewState() + class Error(val error: Throwable, val retry: (() -> Unit)? = null) : ViewState() +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/BaseAdapter.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/BaseAdapter.kt new file mode 100644 index 00000000..3bc13255 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/BaseAdapter.kt @@ -0,0 +1,37 @@ +package br.com.nexaas.common.ui.base.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter + +abstract class BaseAdapter>( + diffCallback: DiffUtil.ItemCallback +) : ListAdapter(diffCallback) { + + @LayoutRes + abstract fun getItemView(viewType: Int): Int + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = DataBindingUtil.inflate( + layoutInflater, + getItemView(viewType), + parent, + false + ) + return instantiateViewHolder(binding, viewType) + } + + override fun onBindViewHolder(holder: VH, position: Int) { + holder.onBind( + item = getItem(position), + position = position + ) + } + + abstract fun instantiateViewHolder(view: ViewDataBinding, viewType: Int): VH +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/BaseViewHolder.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/BaseViewHolder.kt new file mode 100644 index 00000000..68f703fc --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/BaseViewHolder.kt @@ -0,0 +1,10 @@ +package br.com.nexaas.common.ui.base.adapter + +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseViewHolder( + private val binding: ViewDataBinding +) : RecyclerView.ViewHolder(binding.root) { + abstract fun onBind(item: T, position: Int) +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/binding/BindingAdapter.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/binding/BindingAdapter.kt new file mode 100644 index 00000000..e39b4735 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/ui/base/adapter/binding/BindingAdapter.kt @@ -0,0 +1,28 @@ +package br.com.nexaas.common.ui.base.adapter.binding + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.databinding.BindingAdapter +import br.com.nexaas.common.utils.MoneyFormat +import com.bumptech.glide.Glide + +@BindingAdapter("imageUrl") +fun ImageView.imageUrl(url: String?) { + if (url.isNullOrEmpty()) return + Glide.with(context).load(url).into(this) +} + +@BindingAdapter("android:text") +fun TextView.money(value: Long) { + text = MoneyFormat().formatted(value) +} + +@BindingAdapter("isGone") +fun View.bindIsGone(isGone: Boolean) { + visibility = if (isGone) { + View.GONE + } else { + View.VISIBLE + } +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/utils/Collections.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/utils/Collections.kt new file mode 100644 index 00000000..0f664aee --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/utils/Collections.kt @@ -0,0 +1,9 @@ +package br.com.nexaas.common.utils + +inline fun Iterable.sumByLong(selector: (T) -> Long): Long { + var sum = 0L + for (element in this) { + sum += selector(element) + } + return sum +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/utils/Mapper.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/utils/Mapper.kt new file mode 100644 index 00000000..466ab572 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/utils/Mapper.kt @@ -0,0 +1,6 @@ +package br.com.nexaas.common.utils + +interface Mapper { + fun transform(from: T) : R + fun transform(from: List) : List +} \ No newline at end of file diff --git a/Nexaas/common/src/main/java/br/com/nexaas/common/utils/MoneyFormat.kt b/Nexaas/common/src/main/java/br/com/nexaas/common/utils/MoneyFormat.kt new file mode 100644 index 00000000..da9565f4 --- /dev/null +++ b/Nexaas/common/src/main/java/br/com/nexaas/common/utils/MoneyFormat.kt @@ -0,0 +1,18 @@ +package br.com.nexaas.common.utils + +import java.math.BigDecimal +import java.text.NumberFormat +import java.util.* + +class MoneyFormat( + private val locale: Locale = Locale.getDefault() +) { + fun formatted(value: Long): String { + val currency = Currency.getInstance(locale) + val defaultFractionDigits = currency.defaultFractionDigits + val bigDecimal = BigDecimal(value).movePointLeft(defaultFractionDigits) + + val numberFormat = NumberFormat.getCurrencyInstance(locale) + return numberFormat.format(bigDecimal) + } +} \ No newline at end of file diff --git a/Nexaas/common/src/test/java/br/com/nexaas/common/ultis/MoneyFormatTest.kt b/Nexaas/common/src/test/java/br/com/nexaas/common/ultis/MoneyFormatTest.kt new file mode 100644 index 00000000..78645841 --- /dev/null +++ b/Nexaas/common/src/test/java/br/com/nexaas/common/ultis/MoneyFormatTest.kt @@ -0,0 +1,26 @@ +package br.com.nexaas.common.ultis + +import br.com.nexaas.common.utils.MoneyFormat +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.* + +class MoneyFormatTest { + + @Test + fun formatted_BR() { + val value = 10L + val moneyFormat = + MoneyFormat(locale = Locale("pt", "BR")) + assertEquals("R$ 0,10", moneyFormat.formatted(value)) + + } + + @Test + fun formatted_US() { + val value = 10L + val moneyFormat = + MoneyFormat(locale = Locale.US) + assertEquals("$0.10", moneyFormat.formatted(value)) + } +} \ No newline at end of file diff --git a/Nexaas/data/.gitignore b/Nexaas/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Nexaas/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Nexaas/data/build.gradle b/Nexaas/data/build.gradle new file mode 100644 index 00000000..36d5b481 --- /dev/null +++ b/Nexaas/data/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply from: "$rootDir/flavors.gradle" + +android { + compileSdkVersion versions.compileSdkVersion + buildToolsVersion versions.buildToolsVersion + + defaultConfig { + minSdkVersion versions.minSdkVersion + targetSdkVersion versions.targetSdkVersion + versionCode versions.versionCode + versionName versions.versionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + + implementation(project(":common")) + implementation(project(":network")) + implementation(project(":domain")) + implementation(project(":storage")) + + // Storage + kapt deps.androidx.room.compiler + + // Test + testImplementation deps.junit + testImplementation deps.mockito.core + testImplementation deps.hamcrest.library + testImplementation deps.koin.test + testImplementation deps.coroutines.test + testImplementation deps.androidx.test.arch + androidTestImplementation deps.androidx.test.junit + androidTestImplementation deps.androidx.test.espresso + +} \ No newline at end of file diff --git a/Nexaas/data/consumer-rules.pro b/Nexaas/data/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Nexaas/data/proguard-rules.pro b/Nexaas/data/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/Nexaas/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/Nexaas/data/src/androidTest/java/br/com/nexaas/data/ExampleInstrumentedTest.kt b/Nexaas/data/src/androidTest/java/br/com/nexaas/data/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..fde91716 --- /dev/null +++ b/Nexaas/data/src/androidTest/java/br/com/nexaas/data/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package br.com.nexaas.data + +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("br.com.nexaas.data.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/Nexaas/data/src/main/AndroidManifest.xml b/Nexaas/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5f22f0f7 --- /dev/null +++ b/Nexaas/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/di/DataModule.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/di/DataModule.kt new file mode 100644 index 00000000..ad52a925 --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/di/DataModule.kt @@ -0,0 +1,40 @@ +package br.com.nexaas.data.di + +import br.com.nexaas.data.mapper.CartMapper +import br.com.nexaas.data.repository.CartRepositoryImpl +import br.com.nexaas.data.source.local.AppDatabase +import br.com.nexaas.data.source.local.dao.CartDao +import br.com.nexaas.data.source.remote.IApiService +import br.com.nexaas.domain.repository.ICartRepository +import br.com.nexaas.network.NetworkProvider +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module + +object DataModule { + val module = module { + single { + NetworkProvider(get()).createService(IApiService::class.java) + } + + single { + CartRepositoryImpl( + api = get(), + dispatcher = get(), + cartDao = get(), + mapper = get() + ) + } + + single { + CartMapper() + } + + single { + AppDatabase.getInstance(androidApplication()) + } + + single { + get().cartDao() + } + } +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/mapper/CartMapper.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/mapper/CartMapper.kt new file mode 100644 index 00000000..d2e8b3f5 --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/mapper/CartMapper.kt @@ -0,0 +1,62 @@ +package br.com.nexaas.data.mapper + +import br.com.nexaas.data.source.remote.entity.response.CartItemResponse +import br.com.nexaas.domain.entity.CartModel +import br.com.nexaas.domain.mapper.base.DomainMapper +import br.com.nexaas.data.mapper.base.EntityMapper +import br.com.nexaas.data.source.local.entity.CartEntity + +class CartMapper : DomainMapper, EntityMapper { + override fun toDomain(from: CartItemResponse): CartModel { + return CartModel( + quantity = from.quantity, + shipping = from.shipping, + imageUrl = from.imageUrl, + price = from.price, + name = from.name, + description = from.description, + tax = from.tax, + stock = from.stock + ) + } + + override fun toDomain(from: List): List { + return from.map { toDomain(it) } + } + + override fun toEntity(from: CartModel): CartEntity { + return CartEntity( + quantity = from.quantity, + shipping = from.shipping, + imageUrl = from.imageUrl, + price = from.price, + name = from.name, + description = from.description, + tax = from.tax, + stock = from.stock + ) + } + + override fun toEntity(from: List): List { + return from.map { toEntity(it) } + } + + override fun reverseEntity(from: CartEntity): CartModel { + return CartModel( + quantity = from.quantity, + shipping = from.shipping, + imageUrl = from.imageUrl, + price = from.price, + name = from.name, + description = from.description, + tax = from.tax, + stock = from.stock + ) + } + + override fun reverseEntity(from: List): List { + return from.map { reverseEntity(it) } + } + + +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/mapper/base/EntityMapper.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/mapper/base/EntityMapper.kt new file mode 100644 index 00000000..063d28d9 --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/mapper/base/EntityMapper.kt @@ -0,0 +1,8 @@ +package br.com.nexaas.data.mapper.base + +interface EntityMapper { + fun toEntity(from: T) : Entity + fun toEntity(from: List) : List + fun reverseEntity(from: Entity): T + fun reverseEntity(from: List): List +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/repository/CartRepositoryImpl.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/repository/CartRepositoryImpl.kt new file mode 100644 index 00000000..5021ad5b --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/repository/CartRepositoryImpl.kt @@ -0,0 +1,37 @@ +package br.com.nexaas.data.repository + +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import br.com.nexaas.data.mapper.CartMapper +import br.com.nexaas.data.source.local.dao.CartDao +import br.com.nexaas.data.source.remote.IApiService +import br.com.nexaas.domain.entity.CartModel +import br.com.nexaas.domain.repository.ICartRepository +import br.com.nexaas.network.util.apiCall +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext + +class CartRepositoryImpl( + private val api: IApiService, + private val dispatcher: ICoroutinesDispatcherProvider, + private val cartDao: CartDao, + private val mapper: CartMapper +) : ICartRepository { + + override suspend fun getCart(): Flow> { + return flow { + // source local + emit(withContext(dispatcher.io()) { + mapper.reverseEntity(cartDao.getAll()) + }) + // source remote + emit(apiCall(dispatcher) { + val response = api.getCart() + mapper.toDomain(response).also { + cartDao.insert(mapper.toEntity(it)) + } + }) + }.flowOn(dispatcher.io()) + } +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/AppDatabase.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/AppDatabase.kt new file mode 100644 index 00000000..083ecce0 --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/AppDatabase.kt @@ -0,0 +1,21 @@ +package br.com.nexaas.data.source.local + +import android.content.Context +import androidx.room.Database +import androidx.room.RoomDatabase +import br.com.nexaas.data.BuildConfig +import br.com.nexaas.data.source.local.dao.CartDao +import br.com.nexaas.data.source.local.entity.CartEntity +import br.com.nexaas.storage.room.buildDatabase + +@Database(entities = [CartEntity::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + + abstract fun cartDao(): CartDao + + companion object { + fun getInstance(context: Context): AppDatabase { + return buildDatabase(context, BuildConfig.DATABASE_NAME) + } + } +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/dao/CartDao.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/dao/CartDao.kt new file mode 100644 index 00000000..1d5153bb --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/dao/CartDao.kt @@ -0,0 +1,18 @@ +package br.com.nexaas.data.source.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import br.com.nexaas.data.source.local.entity.CartEntity + +@Dao +interface CartDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(items: List) + + @Query("SELECT * FROM CartEntity") + suspend fun getAll(): List + +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/entity/CartEntity.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/entity/CartEntity.kt new file mode 100644 index 00000000..0eb7babf --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/source/local/entity/CartEntity.kt @@ -0,0 +1,34 @@ +package br.com.nexaas.data.source.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "CartEntity") +data class CartEntity( + @ColumnInfo(name = "quantity") + val quantity: Int, + + @ColumnInfo(name = "shipping") + val shipping: Long, + + @ColumnInfo(name = "image_url") + val imageUrl: String, + + @ColumnInfo(name = "price") + val price: Long, + + // TODO warning: API return product ID + @PrimaryKey + @ColumnInfo(name = "name") + val name: String, + + @ColumnInfo(name = "description") + val description: String, + + @ColumnInfo(name = "tax") + val tax: Long, + + @ColumnInfo(name = "stock") + val stock: Int +) \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/source/remote/IApiService.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/source/remote/IApiService.kt new file mode 100644 index 00000000..2e1f8938 --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/source/remote/IApiService.kt @@ -0,0 +1,10 @@ +package br.com.nexaas.data.source.remote + +import br.com.nexaas.data.source.remote.entity.response.CartItemResponse +import retrofit2.http.GET + +interface IApiService { + + @GET("myfreecomm/desafio-mobile-android/master/api/data.json") + suspend fun getCart(): List +} \ No newline at end of file diff --git a/Nexaas/data/src/main/java/br/com/nexaas/data/source/remote/entity/response/CartItemResponse.kt b/Nexaas/data/src/main/java/br/com/nexaas/data/source/remote/entity/response/CartItemResponse.kt new file mode 100644 index 00000000..c130be8a --- /dev/null +++ b/Nexaas/data/src/main/java/br/com/nexaas/data/source/remote/entity/response/CartItemResponse.kt @@ -0,0 +1,32 @@ +package br.com.nexaas.data.source.remote.entity.response + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class CartItemResponse( + + @field:SerializedName("quantity") + val quantity: Int, + + @field:SerializedName("shipping") + val shipping: Long, + + @field:SerializedName("image_url") + val imageUrl: String, + + @field:SerializedName("price") + val price: Long, + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("tax") + val tax: Long, + + @field:SerializedName("stock") + val stock: Int +) \ No newline at end of file diff --git a/Nexaas/data/src/test/java/br/com/nexaas/data/mapper/CartMapperTest.kt b/Nexaas/data/src/test/java/br/com/nexaas/data/mapper/CartMapperTest.kt new file mode 100644 index 00000000..143c3593 --- /dev/null +++ b/Nexaas/data/src/test/java/br/com/nexaas/data/mapper/CartMapperTest.kt @@ -0,0 +1,53 @@ +package br.com.nexaas.data.mapper + +import br.com.nexaas.data.source.remote.entity.response.CartItemResponse +import br.com.nexaas.domain.entity.CartModel +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.Test + +class CartMapperTest { + + private val cartMapper = CartMapper() + + @Test + fun test_toDomain() { + // Given + val responseList = listOf( + CartItemResponse( + name = "Pencil", + quantity = 1, + stock = 5, + imageUrl = "https://github.com/charleston10/test-android-nexaas/blob/master/assets/pencil.png?raw=true", + price = 150, + tax = 162, + shipping = 50, + description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, " + + "gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, " + + "ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odioc." + + ) + ) + + val domainList: List = cartMapper.toDomain(responseList) + + // Then + assertThat( + domainList, contains( + allOf( + hasProperty("name", `is`("Pencil")), + hasProperty("quantity", `is`(1)), + hasProperty("stock", `is`(5)), + hasProperty( + "imageUrl", + `is`("https://github.com/charleston10/test-android-nexaas/blob/master/assets/pencil.png?raw=true") + ), + hasProperty("price", `is`(150L)), + hasProperty("tax", `is`(162L)), + hasProperty("shipping", `is`(50L)) + ) + ) + ) + + } +} \ No newline at end of file diff --git a/Nexaas/data/src/test/java/br/com/nexaas/data/repository/CartRepositoryImplTest.kt b/Nexaas/data/src/test/java/br/com/nexaas/data/repository/CartRepositoryImplTest.kt new file mode 100644 index 00000000..dc6c7c7d --- /dev/null +++ b/Nexaas/data/src/test/java/br/com/nexaas/data/repository/CartRepositoryImplTest.kt @@ -0,0 +1,79 @@ +package br.com.nexaas.data.repository + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import br.com.nexaas.data.di.DataModule +import br.com.nexaas.data.source.local.dao.CartDao +import br.com.nexaas.data.source.local.entity.CartEntity +import br.com.nexaas.data.source.remote.IApiService +import br.com.nexaas.data.source.remote.entity.response.CartItemResponse +import br.com.nexaas.data.util.CoroutinesDispatcherProviderTest +import br.com.nexaas.data.util.getJson +import br.com.nexaas.domain.repository.ICartRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.context.loadKoinModules +import org.koin.core.context.startKoin +import org.koin.dsl.module +import org.koin.test.AutoCloseKoinTest +import org.koin.test.inject +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.MockitoJUnitRunner + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class CartRepositoryImplTest : AutoCloseKoinTest() { + + private val cartRepository: ICartRepository by inject() + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @Mock + private lateinit var mockApiService: IApiService + + @Mock + private lateinit var mockCartDao: CartDao + + @Before + fun setup() { + startKoin { + modules( + DataModule.module + ) + } + loadKoinModules(module(override = true) { + single { + mockApiService + } + single { + CoroutinesDispatcherProviderTest() + } + single { + mockCartDao + } + }) + } + + @Test + fun get_cart() = runBlocking { + // Given + val mockList = getJson>("json/cart.json") + + // When + `when`(mockApiService.getCart()).thenReturn(mockList) + `when`(mockCartDao.getAll()).thenReturn(emptyList()) + + // Then + cartRepository.getCart().collect { + verify(mockApiService).getCart() + verify(mockCartDao).getAll() + } + } +} \ No newline at end of file diff --git a/Nexaas/data/src/test/java/br/com/nexaas/data/util/CoroutinesDispatcherProviderTest.kt b/Nexaas/data/src/test/java/br/com/nexaas/data/util/CoroutinesDispatcherProviderTest.kt new file mode 100644 index 00000000..5d6d3d38 --- /dev/null +++ b/Nexaas/data/src/test/java/br/com/nexaas/data/util/CoroutinesDispatcherProviderTest.kt @@ -0,0 +1,25 @@ +package br.com.nexaas.data.util + +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher + +@ExperimentalCoroutinesApi +class CoroutinesDispatcherProviderTest( + private val dispatcher : TestCoroutineDispatcher = TestCoroutineDispatcher() +) : ICoroutinesDispatcherProvider { + + override fun io(): CoroutineDispatcher { + return dispatcher + } + + override fun ui(): CoroutineDispatcher { + return dispatcher + } + + override fun computation(): CoroutineDispatcher { + return dispatcher + } + +} \ No newline at end of file diff --git a/Nexaas/data/src/test/java/br/com/nexaas/data/util/Utils.kt b/Nexaas/data/src/test/java/br/com/nexaas/data/util/Utils.kt new file mode 100644 index 00000000..31b41670 --- /dev/null +++ b/Nexaas/data/src/test/java/br/com/nexaas/data/util/Utils.kt @@ -0,0 +1,8 @@ +package br.com.nexaas.data.util + +import br.com.nexaas.network.util.fromJson + +inline fun getJson(path: String): T? { + val string = ClassLoader.getSystemResource(path).readText() + return fromJson(string) +} \ No newline at end of file diff --git a/Nexaas/data/src/test/resources/json/cart.json b/Nexaas/data/src/test/resources/json/cart.json new file mode 100644 index 00000000..2a59bf63 --- /dev/null +++ b/Nexaas/data/src/test/resources/json/cart.json @@ -0,0 +1,42 @@ +[ + { + "name": "Pencil", + "quantity": 1, + "stock": 5, + "image_url": "https://github.com/charleston10/test-android-nexaas/blob/master/assets/pencil.png?raw=true", + "price": 150, + "tax": 162, + "shipping": 50, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odioc." + }, + { + "name": "Rubberbands", + "quantity": 1, + "stock": 8, + "image_url": "https://github.com/charleston10/test-android-nexaas/blob/master/assets/rubberbands.png?raw=true", + "price": 450, + "tax": 81, + "shipping": 0, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odio. Nulla ut tincidunt erat, a mollis tortor. Phasellus vel ligula leo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam id semper quam, id efficitur mi. Etiam volutpat eleifend commodo. Duis sed consectetur diam. Morbi mattis justo eget vehicula placerat. Sed commodo, neque a accumsan vehicula, magna libero lacinia dolor, a consectetur turpis odio nec risus. Nullam id dui lacus." + }, + { + "name": "Rulers", + "quantity": 1, + "stock": 1, + "image_url": "https://github.com/charleston10/test-android-nexaas/blob/master/assets/rulers.png?raw=true", + "price": 800, + "tax": 0, + "shipping": 100, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odioc." + }, + { + "name": "Clock", + "quantity": 1, + "stock": 10, + "image_url": "https://github.com/charleston10/test-android-nexaas/blob/master/assets/clock.png?raw=true", + "price": 2200, + "tax": 81, + "shipping": 50, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odio. Nulla ut tincidunt erat, a mollis tortor. Phasellus vel ligula leo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam id semper quam, id efficitur mi. Etiam volutpat eleifend commodo. Duis sed consectetur diam. Morbi mattis justo eget vehicula placerat. Sed commodo, neque a accumsan vehicula, magna libero lacinia dolor, a consectetur turpis odio nec risus. Nullam id dui lacus." + } +] \ No newline at end of file diff --git a/Nexaas/dependencies.gradle b/Nexaas/dependencies.gradle new file mode 100644 index 00000000..98920af9 --- /dev/null +++ b/Nexaas/dependencies.gradle @@ -0,0 +1,111 @@ +ext.versions = [ + kotlin : '1.3.72', + koin : '2.1.0', + retrofit : '2.9.0', + okhttp : '4.7.2', + room : '2.2.5', + coroutines : '1.3.0', + lifecycle : '2.2.0', + navigation : '2.3.0', + glide : '4.11.0', + + + javaVersion : JavaVersion.VERSION_1_8, + minSdkVersion : 21, + targetSdkVersion : 29, + compileSdkVersion: 29, + versionCode : 1, + versionName : '1.0', + buildToolsVersion: '29.0.3' + +] + +ext.deps = [ + kotlin : [ + jdk : "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}", + plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" + ], + + koin : [ + core : "org.koin:koin-core:${versions.koin}", + android : "org.koin:koin-android:${versions.koin}", + viewmodel: "org.koin:koin-android-viewmodel:${versions.koin}", + test : "org.koin:koin-test:${versions.koin}" + ], + + 'retrofit': [ + core : "com.squareup.retrofit2:retrofit:${versions.retrofit}", + converter: "com.squareup.retrofit2:converter-gson:${versions.retrofit}", + ], + + google : [ + material: 'com.google.android.material:material:1.1.0', + gson : 'com.google.code.gson:gson:2.8.6' + ], + + androidx : [ + appcompat : 'androidx.appcompat:appcompat:1.1.0', + core : 'androidx.core:core-ktx:1.3.0', + constraintlayout: 'androidx.constraintlayout:constraintlayout:1.1.3', + annotation : "androidx.annotation:annotation:1.1.0", + + test : [ + junit : "androidx.test.ext:junit:1.1.1", + runner : "androidx.test:runner:1.2.0", + rules : "androidx.test:rules:1.2.0", + espresso: "androidx.test.espresso:espresso-core:3.2.0", + arch : "androidx.arch.core:core-testing:2.1.0" + ], + + room : [ + compiler : "androidx.room:room-compiler:${versions.room}", + runtime : "androidx.room:room-runtime:${versions.room}", + coroutine: "androidx.room:room-ktx:${versions.room}", + testing : "androidx.room:room-testing:${versions.room}" + ], + + lifecycle : [ + extensions: "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}", + viewmodel : "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.lifecycle}", + livedata : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}", + compiler : "android.arch.lifecycle:compiler:${versions.lifecycle}", + savedstate: "androidx.lifecycle:lifecycle-viewmodel-savedstate:${versions.lifecycle}" + ], + + navigation : [ + fragment : "androidx.navigation:navigation-fragment-ktx:${versions.navigation}", + ui : "androidx.navigation:navigation-ui-ktx:${versions.navigation}", + dynamicfeatures: "androidx.navigation:navigation-dynamic-features-fragment:${versions.navigation}", + testing : "androidx.navigation:navigation-testing:${versions.navigation}", + plugin : "androidx.navigation:navigation-safe-args-gradle-plugin:${versions.navigation}" + ] + ], + + multidex : 'com.android.support:multidex:1.0.3', + + junit : 'junit:junit:4.13', + + mockito : [ + core: 'org.mockito:mockito-core:3.2.4' + ], + + hamcrest : [ + library: "org.hamcrest:hamcrest-library:2.2" + ], + + coroutines: [ + core : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}", + android: "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}", + test : "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.coroutines}" + ], + + 'okhttp' : [ + core : "com.squareup.okhttp3:okhttp:${versions.okhttp}", + logging: "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}" + ], + + glide : [ + core : "com.github.bumptech.glide:glide:${versions.glide}", + compiler: "com.github.bumptech.glide:compiler:${versions.glide}" + ] +] \ No newline at end of file diff --git a/Nexaas/domain/.gitignore b/Nexaas/domain/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Nexaas/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Nexaas/domain/build.gradle b/Nexaas/domain/build.gradle new file mode 100644 index 00000000..3bd5ad44 --- /dev/null +++ b/Nexaas/domain/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'java-library' +apply plugin: 'kotlin' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + // Kotlin + implementation deps.kotlin.jdk + // DI + implementation deps.koin.core + + // Coroutines + implementation deps.coroutines.core + + // Test + testImplementation deps.junit + testImplementation deps.mockito.core + testImplementation deps.hamcrest.library + testImplementation deps.koin.test + testImplementation deps.coroutines.test +} + +sourceCompatibility = versions.javaVersion +targetCompatibility = versions.javaVersion \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/di/DomainModule.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/di/DomainModule.kt new file mode 100644 index 00000000..4875017e --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/di/DomainModule.kt @@ -0,0 +1,13 @@ +package br.com.nexaas.domain.di + +import br.com.nexaas.domain.usecase.cart.GetCartUseCaseImpl +import br.com.nexaas.domain.usecase.cart.IGetCartUseCase +import org.koin.dsl.module + +object DomainModule { + val module = module { + single { + GetCartUseCaseImpl(repository = get()) + } + } +} \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/entity/CartModel.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/entity/CartModel.kt new file mode 100644 index 00000000..96f9c265 --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/entity/CartModel.kt @@ -0,0 +1,12 @@ +package br.com.nexaas.domain.entity + +data class CartModel( + val quantity: Int, + val shipping: Long, + val imageUrl: String, + val price: Long, + val name: String, + val description: String, + val tax: Long, + val stock: Int +) \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/mapper/base/DomainMapper.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/mapper/base/DomainMapper.kt new file mode 100644 index 00000000..d9c33a0e --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/mapper/base/DomainMapper.kt @@ -0,0 +1,6 @@ +package br.com.nexaas.domain.mapper.base + +interface DomainMapper { + fun toDomain(from: T) : Model + fun toDomain(from: List) : List +} \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/repository/ICartRepository.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/repository/ICartRepository.kt new file mode 100644 index 00000000..ecdb9a52 --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/repository/ICartRepository.kt @@ -0,0 +1,8 @@ +package br.com.nexaas.domain.repository + +import br.com.nexaas.domain.entity.CartModel +import kotlinx.coroutines.flow.Flow + +interface ICartRepository { + suspend fun getCart(): Flow> +} \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/cart/GetCartUseCaseImpl.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/cart/GetCartUseCaseImpl.kt new file mode 100644 index 00000000..51fec4f7 --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/cart/GetCartUseCaseImpl.kt @@ -0,0 +1,11 @@ +package br.com.nexaas.domain.usecase.cart + +import br.com.nexaas.domain.entity.CartModel +import br.com.nexaas.domain.repository.ICartRepository +import kotlinx.coroutines.flow.Flow + +class GetCartUseCaseImpl(private val repository: ICartRepository) : IGetCartUseCase { + override suspend fun execute(): Flow> { + return repository.getCart() + } +} \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/cart/IGetCartUseCase.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/cart/IGetCartUseCase.kt new file mode 100644 index 00000000..827fc482 --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/cart/IGetCartUseCase.kt @@ -0,0 +1,7 @@ +package br.com.nexaas.domain.usecase.cart + +import br.com.nexaas.domain.entity.CartModel +import br.com.nexaas.domain.usecase.core.UseCase +import kotlinx.coroutines.flow.Flow + +interface IGetCartUseCase : UseCase.WithoutParameter>> \ No newline at end of file diff --git a/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/core/UseCase.kt b/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/core/UseCase.kt new file mode 100644 index 00000000..f1bf13b2 --- /dev/null +++ b/Nexaas/domain/src/main/java/br/com/nexaas/domain/usecase/core/UseCase.kt @@ -0,0 +1,12 @@ +package br.com.nexaas.domain.usecase.core + +interface UseCase { + + interface WithParameter { + suspend fun execute(params: P): R + } + + interface WithoutParameter { + suspend fun execute(): R + } +} \ No newline at end of file diff --git a/Nexaas/domain/src/test/java/br/com/nexaas/domain/usecase/cart/GetCartUseCaseImplTest.kt b/Nexaas/domain/src/test/java/br/com/nexaas/domain/usecase/cart/GetCartUseCaseImplTest.kt new file mode 100644 index 00000000..906181ea --- /dev/null +++ b/Nexaas/domain/src/test/java/br/com/nexaas/domain/usecase/cart/GetCartUseCaseImplTest.kt @@ -0,0 +1,92 @@ +package br.com.nexaas.domain.usecase.cart + +import br.com.nexaas.domain.di.DomainModule +import br.com.nexaas.domain.entity.CartModel +import br.com.nexaas.domain.repository.ICartRepository +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.hasItems +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.context.startKoin +import org.koin.dsl.module +import org.koin.test.AutoCloseKoinTest +import org.koin.test.inject +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +internal class GetCartUseCaseImplTest : AutoCloseKoinTest() { + + private val getCartUseCase: IGetCartUseCase by inject() + + @Mock + private lateinit var mockCartRepository: ICartRepository + + @Before + fun setup() { + startKoin { + modules( + module { + single { + mockCartRepository + } + }, + DomainModule.module + ) + } + } + + @Test + fun get_cart() = runBlocking { + + // Given + val item = CartModel( + name = "Pencil", + quantity = 1, + stock = 5, + imageUrl = "https://github.com/charleston10/test-android-nexaas/blob/master/assets/pencil.png?raw=true", + price = 150, + tax = 162, + shipping = 50, + description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, " + + "gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, " + + "ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odioc." + + ) + val mockList = listOf( + CartModel( + name = "Pencil", + quantity = 1, + stock = 5, + imageUrl = "https://github.com/charleston10/test-android-nexaas/blob/master/assets/pencil.png?raw=true", + price = 150, + tax = 162, + shipping = 50, + description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam nunc magna, " + + "gravida ut orci non, egestas venenatis libero. Sed luctus, turpis at porta commodo, " + + "ipsum orci volutpat sapien, ut scelerisque diam massa lobortis odioc." + ) + ) + + val flow = flow { + emit(mockList) + } + + // When + `when`(mockCartRepository.getCart()).thenReturn(flow) + + // Then + getCartUseCase.execute().collect { list-> + assertEquals(1, list.size) + assertThat(list, hasItems(item)) + } + } + +} \ No newline at end of file diff --git a/Nexaas/flavors.gradle b/Nexaas/flavors.gradle new file mode 100644 index 00000000..693d341e --- /dev/null +++ b/Nexaas/flavors.gradle @@ -0,0 +1,35 @@ +android { + + flavorDimensions "default" + + productFlavors { + + dev { + versionNameSuffix " DEV" + dimension "default" + // NETWORK + buildConfigField "String", "BASE_URL", "\"https://raw.githubusercontent.com\"" + + // STORAGE + buildConfigField "String", "DATABASE_NAME", "\"data-db-dev\"" + } + + prod { + dimension "default" + // NETWORK + buildConfigField "String", "BASE_URL", "\"https://raw.githubusercontent.com\"" + + // STORAGE + buildConfigField "String", "DATABASE_NAME", "\"data-db\"" + } + } + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + kotlinOptions { + jvmTarget = versions.javaVersion + } +} \ No newline at end of file diff --git a/Nexaas/gradle.properties b/Nexaas/gradle.properties new file mode 100644 index 00000000..20b98449 --- /dev/null +++ b/Nexaas/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# https://developer.android.com/jetpack/androidx/releases/room#compiler-options +kapt.incremental.apt=true \ No newline at end of file diff --git a/Nexaas/gradle/wrapper/gradle-wrapper.jar b/Nexaas/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f6b961fd Binary files /dev/null and b/Nexaas/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Nexaas/gradle/wrapper/gradle-wrapper.properties b/Nexaas/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..f73243ff --- /dev/null +++ b/Nexaas/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 07 23:55:18 BRT 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/Nexaas/network/.gitignore b/Nexaas/network/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Nexaas/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Nexaas/network/build.gradle b/Nexaas/network/build.gradle new file mode 100644 index 00000000..0873afb5 --- /dev/null +++ b/Nexaas/network/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply from: "$rootDir/flavors.gradle" + +android { + compileSdkVersion versions.compileSdkVersion + buildToolsVersion versions.buildToolsVersion + + defaultConfig { + minSdkVersion versions.minSdkVersion + targetSdkVersion versions.targetSdkVersion + versionCode versions.versionCode + versionName versions.versionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + + implementation(project(":common")) + + // Networking + api deps.retrofit.core + api deps.retrofit.converter + implementation deps.okhttp.core + implementation deps.okhttp.logging + + // Test + testImplementation deps.junit + androidTestImplementation deps.androidx.test.junit + androidTestImplementation deps.androidx.test.espresso + +} \ No newline at end of file diff --git a/Nexaas/network/consumer-rules.pro b/Nexaas/network/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Nexaas/network/proguard-rules.pro b/Nexaas/network/proguard-rules.pro new file mode 100644 index 00000000..6a0331b5 --- /dev/null +++ b/Nexaas/network/proguard-rules.pro @@ -0,0 +1,42 @@ +# 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 + + +#### OkHttp #### +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt dependency is available. +-dontwarn okhttp3.internal.platform.ConscryptPlatform +################ + + +#### Okio #### +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* +################ \ No newline at end of file diff --git a/Nexaas/network/src/androidTest/java/br/com/nexaas/network/ExampleInstrumentedTest.kt b/Nexaas/network/src/androidTest/java/br/com/nexaas/network/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..99764b8f --- /dev/null +++ b/Nexaas/network/src/androidTest/java/br/com/nexaas/network/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package br.com.nexaas.network + +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("br.com.nexaas.network.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/Nexaas/network/src/main/AndroidManifest.xml b/Nexaas/network/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d8cb6ebc --- /dev/null +++ b/Nexaas/network/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/Nexaas/network/src/main/java/br/com/nexaas/network/NetworkProvider.kt b/Nexaas/network/src/main/java/br/com/nexaas/network/NetworkProvider.kt new file mode 100644 index 00000000..28322a0e --- /dev/null +++ b/Nexaas/network/src/main/java/br/com/nexaas/network/NetworkProvider.kt @@ -0,0 +1,11 @@ +package br.com.nexaas.network + +import retrofit2.Retrofit + +class NetworkProvider(private val retrofit: Retrofit) { + + fun createService(serviceClass: Class): T { + return retrofit.create(serviceClass) + } + +} \ No newline at end of file diff --git a/Nexaas/network/src/main/java/br/com/nexaas/network/di/NetworkModule.kt b/Nexaas/network/src/main/java/br/com/nexaas/network/di/NetworkModule.kt new file mode 100644 index 00000000..64f5d3b8 --- /dev/null +++ b/Nexaas/network/src/main/java/br/com/nexaas/network/di/NetworkModule.kt @@ -0,0 +1,43 @@ +package br.com.nexaas.network.di + +import br.com.nexaas.network.BuildConfig +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.dsl.module +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object NetworkModule { + val module = module { + + single { + Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(get()) + .client(get()) + .build() + } + + single { + val httpClient = OkHttpClient.Builder().apply { + if (BuildConfig.DEBUG) { + addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + } + } + httpClient.build() + } + + single { + val build = GsonBuilder() + .setLenient() + .setPrettyPrinting() + .create() + GsonConverterFactory.create(build) + } + + } +} \ No newline at end of file diff --git a/Nexaas/network/src/main/java/br/com/nexaas/network/util/CallUtils.kt b/Nexaas/network/src/main/java/br/com/nexaas/network/util/CallUtils.kt new file mode 100644 index 00000000..9aeb7759 --- /dev/null +++ b/Nexaas/network/src/main/java/br/com/nexaas/network/util/CallUtils.kt @@ -0,0 +1,26 @@ +package br.com.nexaas.network.util + +import br.com.nexaas.common.coroutines.ICoroutinesDispatcherProvider +import kotlinx.coroutines.withContext +import retrofit2.HttpException + +suspend inline fun apiCall( + dispatcher: ICoroutinesDispatcherProvider, + crossinline block: suspend () -> T +): T { + return withContext(dispatcher.io()) { + try { + block() + } catch (error: Throwable) { + throw when (error) { + is HttpException -> { + // TODO parse error body to API exception + error + } + else -> { + error + } + } + } + } +} \ No newline at end of file diff --git a/Nexaas/network/src/main/java/br/com/nexaas/network/util/GsonUtils.kt b/Nexaas/network/src/main/java/br/com/nexaas/network/util/GsonUtils.kt new file mode 100644 index 00000000..89f02d94 --- /dev/null +++ b/Nexaas/network/src/main/java/br/com/nexaas/network/util/GsonUtils.kt @@ -0,0 +1,9 @@ +package br.com.nexaas.network.util + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +inline fun fromJson(json: String?): T? { + val type = object : TypeToken() {}.type + return Gson().fromJson(json, type) +} \ No newline at end of file diff --git a/Nexaas/network/src/test/java/br/com/nexaas/network/ExampleUnitTest.kt b/Nexaas/network/src/test/java/br/com/nexaas/network/ExampleUnitTest.kt new file mode 100644 index 00000000..13a8250d --- /dev/null +++ b/Nexaas/network/src/test/java/br/com/nexaas/network/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package br.com.nexaas.network + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/Nexaas/screenshots/screenshot_cart.png b/Nexaas/screenshots/screenshot_cart.png new file mode 100644 index 00000000..da067e57 Binary files /dev/null and b/Nexaas/screenshots/screenshot_cart.png differ diff --git a/Nexaas/screenshots/screenshot_product_details.png b/Nexaas/screenshots/screenshot_product_details.png new file mode 100644 index 00000000..5d6c872f Binary files /dev/null and b/Nexaas/screenshots/screenshot_product_details.png differ diff --git a/Nexaas/settings.gradle b/Nexaas/settings.gradle new file mode 100644 index 00000000..0bfc006f --- /dev/null +++ b/Nexaas/settings.gradle @@ -0,0 +1,7 @@ +include ':storage' +include ':network' +include ':common' +include ':data' +include ':domain' +include ':app' +rootProject.name = "Nexaas" \ No newline at end of file diff --git a/Nexaas/storage/.gitignore b/Nexaas/storage/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/Nexaas/storage/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Nexaas/storage/build.gradle b/Nexaas/storage/build.gradle new file mode 100644 index 00000000..3542982a --- /dev/null +++ b/Nexaas/storage/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply from: "$rootDir/flavors.gradle" + +android { + compileSdkVersion versions.compileSdkVersion + buildToolsVersion versions.buildToolsVersion + + defaultConfig { + minSdkVersion versions.minSdkVersion + targetSdkVersion versions.targetSdkVersion + versionCode versions.versionCode + versionName versions.versionName + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation deps.kotlin.jdk + + // Room + kapt deps.androidx.room.compiler + api deps.androidx.room.runtime + api deps.androidx.room.coroutine + + // Test + testImplementation deps.junit + androidTestImplementation deps.androidx.test.junit + androidTestImplementation deps.androidx.test.espresso + +} \ No newline at end of file diff --git a/Nexaas/storage/consumer-rules.pro b/Nexaas/storage/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Nexaas/storage/proguard-rules.pro b/Nexaas/storage/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/Nexaas/storage/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/Nexaas/storage/src/androidTest/java/br/com/nexaas/storage/ExampleInstrumentedTest.kt b/Nexaas/storage/src/androidTest/java/br/com/nexaas/storage/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..233a4907 --- /dev/null +++ b/Nexaas/storage/src/androidTest/java/br/com/nexaas/storage/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package br.com.nexaas.storage + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * 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("br.com.nexaas.storage.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/Nexaas/storage/src/main/AndroidManifest.xml b/Nexaas/storage/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a2cda0a9 --- /dev/null +++ b/Nexaas/storage/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/Nexaas/storage/src/main/java/br/com/nexaas/storage/room/RoomUtils.kt b/Nexaas/storage/src/main/java/br/com/nexaas/storage/room/RoomUtils.kt new file mode 100644 index 00000000..8eca9936 --- /dev/null +++ b/Nexaas/storage/src/main/java/br/com/nexaas/storage/room/RoomUtils.kt @@ -0,0 +1,17 @@ +package br.com.nexaas.storage.room + +import android.content.Context +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.sqlite.db.SupportSQLiteDatabase + +inline fun buildDatabase(context: Context, name: String): T { + return Room.databaseBuilder(context, T::class.java, name) + .addCallback(object : RoomDatabase.Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + println("onCreate Database") + } + }) + .build() +} \ No newline at end of file diff --git a/Nexaas/storage/src/test/java/br/com/nexaas/storage/ExampleUnitTest.kt b/Nexaas/storage/src/test/java/br/com/nexaas/storage/ExampleUnitTest.kt new file mode 100644 index 00000000..996a014b --- /dev/null +++ b/Nexaas/storage/src/test/java/br/com/nexaas/storage/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package br.com.nexaas.storage + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file