Skip to content

Commit

Permalink
Merge branch 'release/v1.0.0-alpha01'
Browse files Browse the repository at this point in the history
  • Loading branch information
SmartToolFactory committed Sep 9, 2020
2 parents 0a406c4 + 94d420f commit 5211e27
Show file tree
Hide file tree
Showing 62 changed files with 2,105 additions and 428 deletions.
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# MVVM Clean Architecture with RxJava3+Coroutines Flow, Static Code Analysis, Dagger Hilt, Dynamic Features

[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
[![Kotlin Version](https://img.shields.io/badge/kotlin-1.4.0-blue.svg)](https://kotlinlang.org)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
[![codecov](https://codecov.io/gh/andremion/Theatre/graph/badge.svg)](https://codecov.io/gh/andremion/Theatre)


## About

Sample project that build with MVVM clean architure and various cool techs including RxJava3 and Coroutines Flow, Dynamic Feature modules as base of BottomNavigationView or ViewPager2, with both OfflineFirst and OfflineLast approaches as database Single Source of Truth and TDD.

Unit tests are written with JUnit4, JUnit5, MockK, Truth, MockWebServer.

| Flow | RxJava3 | Pagination | Favorites
| ------------------|-------------| -----|--------------|
| <img src="./screenshots/property_flow.png"/> | <img src="./screenshots/property_rxjava3.png"/> | <img src="./screenshots/property_pagination.png"/> |<img src="./screenshots/favorites.png"/> |


## Overview
* Gradle Kotlin DSL is used for setting up gradle files with ```buildSrc``` folder and extensions.
* KtLint, Detekt, and Git Hooks is used for checking, and formatting code and validating code before commits.
* Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, RxJava, Coroutines libraries adn dependencies are set up.
* ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules
* In core module dagger hilt dependencies and ```@EntryPoint``` is created
* Data module uses Retrofit and Room to provide Local and Remote data sources
* Repository provides offline and remote fetch function with mapping and local save, delete and fetch functions
* Domain module uses useCase classes to implment business logic to fetch and forward data
* ViewModel uses LiveData with data-binding to display LOADING, and ERROR or SUCCESS states.

## Built With 🛠

Some of the popular libraries and MVVM clean architecture used with offline-first and offline-last with Room database and Retrofit as data source

* [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android development.

* [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - Threads on steroids for Kotlin
* [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/) - A cold asynchronous data stream that sequentially emits values and completes normally or with an exception.
* [RxJava3](https://github.com/ReactiveX/RxJava) - Newest version of famous reactive programming library for Java, and other languages
* [Android JetPack](https://developer.android.com/jetpack) - Collection of libraries that help you design robust, testable, and maintainable apps.
* [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes.
* [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes.
* [DataBinding](https://developer.android.com/topic/libraries/data-binding) - Generates a binding class for each XML layout file present in that module and allows you to more easily write code that interacts with views.
* [Navigation Components](https://developer.android.com/guide/navigation/navigation-getting-started) Navigate fragments as never easier before
* [Dynamic Feature Modules](https://developer.android.com/guide/playcore/dynamic-delivery) Dynamic modules for adding or removing based on preference
* [Material Components for Android](https://github.com/material-components/material-components-android) - Modular and customizable Material Design UI components for Android.
* [Dependency Injection](https://developer.android.com/training/dependency-injection) -
* [Hilt-Dagger](https://dagger.dev/hilt/) - Standard library to incorporate Dagger dependency injection into an Android application.
* [Hilt-ViewModel](https://developer.android.com/training/dependency-injection/hilt-jetpack) - DI for injecting `ViewModel`.
* [Retrofit](https://square.github.io/retrofit/) - A type-safe HTTP client for Android and Java.
* [Glide](https://github.com/bumptech/glide) - Image loading library.
* [Lottie](http://airbnb.io/lottie) - animation library

* Architecture
* Clean Architecture
* MVVM + MVI
* Offline first/last with Room an Retrofit
* [Dynamic feature modules](https://developer.android.com/studio/projects/dynamic-delivery)
* Tests
* [Unit Tests](https://en.wikipedia.org/wiki/Unit_testing) ([JUnit5](https://junit.org/junit5/)) ([JUnit4](https://junit.org/junit4/))
* [MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) Mock server for testing Api requests with OkHttp and Retrofit
* [Mockk](https://mockk.io/) Mockking library for Kotlin
* [Truth](https://truth.dev) Assertion library
* Gradle
* [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html)
* Custom tasks
* Plugins ([Ktlint](https://github.com/JLLeitschuh/ktlint-gradle), [Detekt](https://github.com/arturbosch/detekt#with-gradle), [SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)), [Git Hooks](https://githooks.com)


### Modularaization, Library and Feature Modules

Uses both library modules and dynamic feature modules

## Architecture

Uses concepts of clean architecture


2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<resources>
<string name="app_name">PropertyFindAR</string>
<string name="app_name">Property FindAR</string>
<string name="title_home">Home</string>
<string name="title_favorites">Favorites</string>
<string name="title_notification">Notification</string>
Expand Down
9 changes: 3 additions & 6 deletions features/account/src/main/res/layout/fragment_account.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,20 @@
android:text="ACCOUNT\n Under Construction"
android:textSize="30dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/lavUnderConstruction"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
app:layout_constraintTop_toTopOf="parent" />

<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lavUnderConstruction"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintVertical_bias="0.4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent=".6"
app:lottie_autoPlay="true"
app:lottie_fileName="construction_process.json" />
Expand Down
9 changes: 4 additions & 5 deletions features/favorites/src/main/res/layout/fragment_favorites.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
Expand All @@ -14,22 +15,20 @@
android:textSize="30dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/lavUnderConstruction"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
app:layout_constraintTop_toTopOf="parent" />

<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lavUnderConstruction"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintVertical_bias="0.4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent=".6"
app:lottie_autoPlay="true"
app:lottie_fileName="construction_process.json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,22 @@ class HomeViewPager2FragmentStateAdapter(fragmentManager: FragmentManager, lifec
override fun createFragment(position: Int): Fragment {

return when (position) {

// Fragment with Flow
0 -> NavHostContainerFragment.createNavHostContainerFragment(
R.layout.fragment_navhost_property_list_flow,
R.id.nested_nav_host_fragment_property_list
)

// Fragment with Pagination
else -> NavHostContainerFragment.createNavHostContainerFragment(
// Fragment with RxJava3
1 -> NavHostContainerFragment.createNavHostContainerFragment(
R.layout.fragment_navhost_property_list_rxjava3,
R.id.nested_nav_host_fragment_property_list
)

// Fragment with Flow + Pagination
else -> NavHostContainerFragment.createNavHostContainerFragment(
R.layout.fragment_navhost_property_list_paged,
R.id.nested_nav_host_fragment_property_list
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.smarttoolfactory.home.adapter

class LoadingAdapter
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.smarttoolfactory.home.adapter

import android.graphics.Color
import android.widget.ImageButton
import androidx.annotation.LayoutRes
import androidx.databinding.ViewDataBinding
Expand Down Expand Up @@ -57,12 +58,15 @@ class PropertyItemListAdapter(
onLikeButtonClick(this)

// Set image source of like button
val likeImageButton = (likeButton as? ImageButton)
val imageResource = if (isFavorite) {
R.drawable.ic_baseline_favorite_24
likeImageButton?.setColorFilter(Color.rgb(244, 81, 30))
R.drawable.ic_baseline_favorite_30
} else {
R.drawable.ic_baseline_favorite_border_24
likeImageButton?.setColorFilter(Color.rgb(41, 182, 246))
R.drawable.ic_baseline_favorite_border_30
}
(likeButton as? ImageButton)?.setImageResource(imageResource)
likeImageButton?.setImageResource(imageResource)
}
}
}
Expand All @@ -78,17 +82,11 @@ class PropertyItemListAdapter(
*/
class PropertyItemDiffCallback : DiffUtil.ItemCallback<PropertyItem>() {

override fun areItemsTheSame(
oldItem: PropertyItem,
newItem: PropertyItem
): Boolean {
return oldItem === newItem
override fun areItemsTheSame(oldItem: PropertyItem, newItem: PropertyItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(
oldItem: PropertyItem,
newItem: PropertyItem
): Boolean {
return oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: PropertyItem, newItem: PropertyItem): Boolean {
return oldItem == newItem
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package com.smarttoolfactory.home.di

import androidx.fragment.app.Fragment
import com.smarttoolfactory.core.di.CoreModuleDependencies
import com.smarttoolfactory.home.propertylist.PropertyListFlowFragment
import com.smarttoolfactory.home.propertylist.PropertyListRxjava3Fragment
import com.smarttoolfactory.home.propertylist.flow.PropertyListFlowFragment
import com.smarttoolfactory.home.propertylist.paged.PagedPropertyListFragment
import com.smarttoolfactory.home.propertylist.rxjava.PropertyListRxjava3Fragment
import dagger.BindsInstance
import dagger.Component

Expand All @@ -15,6 +16,7 @@ interface HomeComponent {

fun inject(fragment: PropertyListFlowFragment)
fun inject(fragment: PropertyListRxjava3Fragment)
fun inject(fragment: PagedPropertyListFragment)

@Component.Factory
interface Factory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.smarttoolfactory.home.di

import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.smarttoolfactory.home.propertylist.flow.PropertyListViewModelFlow
import com.smarttoolfactory.home.propertylist.paged.PagedPropertyListViewModel
import com.smarttoolfactory.home.propertylist.rxjava.PropertyListViewModelRxJava3
import com.smarttoolfactory.home.viewmodel.PagedPropertyListViewModelFactory
import com.smarttoolfactory.home.viewmodel.PropertyListFlowViewModelFactory
import com.smarttoolfactory.home.viewmodel.PropertyListRxJava3ViewModelFactory
import com.smarttoolfactory.home.viewmodel.PropertyListViewModelFlow
import com.smarttoolfactory.home.viewmodel.PropertyListViewModelRxJava3
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -18,13 +20,29 @@ import kotlinx.coroutines.SupervisorJob
@Module
class HomeModule {

/**
* Property ViewModel that uses Flow for data operation
*/
@Provides
fun providePropertyListViewModelFlow(
fragment: Fragment,
factory: PropertyListFlowViewModelFactory
) =
ViewModelProvider(fragment, factory).get(PropertyListViewModelFlow::class.java)

/**
* Property ViewModel that uses Flow for data operation with Pagaination
*/
@Provides
fun providePagedPropertyListViewModel(
fragment: Fragment,
factory: PagedPropertyListViewModelFactory
) =
ViewModelProvider(fragment, factory).get(PagedPropertyListViewModel::class.java)

/**
* Property ViewModel that uses Rxjava for data operations
*/
@Provides
fun providePropertyListViewModelRxJava3(
fragment: Fragment,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.smarttoolfactory.home.viewmodel
package com.smarttoolfactory.home.propertylist

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.smarttoolfactory.home.propertylist
package com.smarttoolfactory.home.propertylist.flow

import android.os.Bundle
import androidx.core.os.bundleOf
Expand All @@ -12,7 +12,6 @@ import com.smarttoolfactory.home.adapter.PropertyItemListAdapter
import com.smarttoolfactory.home.databinding.FragmentPropertyListBinding
import com.smarttoolfactory.home.di.DaggerHomeComponent
import com.smarttoolfactory.home.viewmodel.HomeToolbarVM
import com.smarttoolfactory.home.viewmodel.PropertyListViewModelFlow
import dagger.hilt.android.EntryPointAccessors
import javax.inject.Inject

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.smarttoolfactory.home.viewmodel
package com.smarttoolfactory.home.propertylist.flow

import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.LiveData
Expand All @@ -10,7 +10,11 @@ import com.smarttoolfactory.core.viewstate.ViewState
import com.smarttoolfactory.domain.ORDER_BY_NONE
import com.smarttoolfactory.domain.model.PropertyItem
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
Expand All @@ -34,17 +38,17 @@ class PropertyListViewModelFlow @ViewModelInject constructor(

var orderKey = MutableLiveData<String>().apply { value = _orderByKey }

init {
updateOrderByKey()
}

fun updateOrderByKey() {
getPropertiesUseCase.getCurrentSortKey()
private fun getOrderByKey(): Flow<String?> {
return getPropertiesUseCase.getCurrentSortKey()
.onEach {
_orderByKey = it
orderKey.value = _orderByKey
println("🍏 AbstractPropertyListVM init orderKey: $it")
_orderByKey = it ?: _orderByKey
orderKey.postValue(_orderByKey)
}
.catch {
orderKey.postValue(_orderByKey)
println("❌ AbstractPropertyListVM init error: $it")
}
.launchIn(coroutineScope)
}

/**
Expand All @@ -60,12 +64,18 @@ class PropertyListViewModelFlow @ViewModelInject constructor(
*/
override fun getPropertyList() {

getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)
getOrderByKey()
.flatMapConcat {
getPropertiesUseCase
.getPropertiesOfflineFirst(_orderByKey)
}
.convertToFlowViewState()
.onStart {
println("🍏 FlowViewModel getPropertyList() START")
_propertyViewState.value = ViewState(status = Status.LOADING)
}
.onEach {
println("🍎 FlowViewModel getPropertyList() RES: $it")
_propertyViewState.value = it
}
.launchIn(coroutineScope)
Expand Down
Loading

0 comments on commit 5211e27

Please sign in to comment.