├── cache
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── mi
│ │ └── mvi
│ │ └── cache
│ │ ├── mapper
│ │ ├── EntityMapper.kt
│ │ ├── UserEntityMapper.kt
│ │ ├── TokenEntityMapper.kt
│ │ └── BlogPostEntityMapper.kt
│ │ ├── model
│ │ ├── CachedUser.kt
│ │ ├── CachedToken.kt
│ │ └── CachedBlogPost.kt
│ │ ├── koin
│ │ ├── SharedPrefsModule.kt
│ │ └── DataBaseModule.kt
│ │ ├── db
│ │ ├── AuthTokenDao.kt
│ │ ├── AppDatabase.kt
│ │ ├── AccountDao.kt
│ │ └── BlogPostDao.kt
│ │ └── source
│ │ ├── TokenCacheDataSourceImpl.kt
│ │ └── AccountCacheDataSourceImpl.kt
├── build.gradle.kts
└── proguard-rules.pro
├── data
├── .gitignore
├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── mi
│ │ └── mvi
│ │ └── data
│ │ ├── entity
│ │ ├── TokenEntity.kt
│ │ ├── BaseEntity.kt
│ │ ├── BlogPostListEntity.kt
│ │ ├── UserEntity.kt
│ │ └── BlogPostEntity.kt
│ │ ├── datasource
│ │ ├── cache
│ │ │ ├── TokenCacheDataSource.kt
│ │ │ ├── AccountCacheDataSource.kt
│ │ │ ├── BlogCacheQueries.kt
│ │ │ └── BlogCacheDataSource.kt
│ │ └── remote
│ │ │ ├── AuthRemoteDataSource.kt
│ │ │ ├── AccountRemoteDataSource.kt
│ │ │ └── BlogRemoteDataSource.kt
│ │ ├── mapper
│ │ ├── Mapper.kt
│ │ ├── TokenMapper.kt
│ │ ├── BaseMapper.kt
│ │ ├── UserMapper.kt
│ │ └── BlogPostMapper.kt
│ │ ├── koin
│ │ └── DataModule.kt
│ │ └── repository
│ │ └── CreateBlogRepositoryImpl.kt
├── build.gradle.kts
└── proguard-rules.pro
├── domain
├── .gitignore
├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── mi
│ │ └── mvi
│ │ └── domain
│ │ ├── repository
│ │ ├── BaseRepository.kt
│ │ ├── CreateBlogRepository.kt
│ │ ├── AuthRepository.kt
│ │ ├── AccountRepository.kt
│ │ └── BlogRepository.kt
│ │ ├── model
│ │ ├── Token.kt
│ │ ├── BaseModel.kt
│ │ ├── BlogPostList.kt
│ │ ├── User.kt
│ │ └── BlogPost.kt
│ │ ├── viewstate
│ │ ├── AccountViewState.kt
│ │ ├── CreateBlogViewState.kt
│ │ ├── BlogViewState.kt
│ │ └── AuthViewState.kt
│ │ ├── usecase
│ │ ├── blogs
│ │ │ ├── FiltrationUseCase.kt
│ │ │ ├── IsAuthorBlogPostUseCase.kt
│ │ │ ├── DeleteBlogPostUseCase.kt
│ │ │ ├── SearchBlogUseCase.kt
│ │ │ ├── UpdateBlogPostUseCase.kt
│ │ │ └── CreateBlogUseCase.kt
│ │ ├── auth
│ │ │ ├── CheckTokenUseCase.kt
│ │ │ ├── LoginUseCase.kt
│ │ │ └── RegisterUseCase.kt
│ │ └── account
│ │ │ ├── GetAccountUseCase.kt
│ │ │ ├── UpdateAccountUseCase.kt
│ │ │ └── ChangePasswordUseCase.kt
│ │ ├── datastate
│ │ ├── StateResource.kt
│ │ └── DataState.kt
│ │ ├── koin
│ │ └── UseCaseModule.kt
│ │ └── Constants.kt
├── build.gradle.kts
└── proguard-rules.pro
├── remote
├── .gitignore
├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── mi
│ │ └── mvi
│ │ └── remote
│ │ ├── model
│ │ ├── BaseRemote.kt
│ │ ├── RemoteBlogPostList.kt
│ │ ├── RemoteUser.kt
│ │ └── RemoteBlogPost.kt
│ │ ├── mapper
│ │ ├── EntityMapper.kt
│ │ ├── BaseEntityMapper.kt
│ │ ├── BlogPostEntityMapper.kt
│ │ ├── UserEntityMapper.kt
│ │ └── BlogPostListEntityMapper.kt
│ │ ├── service
│ │ ├── AuthAPIService.kt
│ │ ├── AccountAPIService.kt
│ │ └── BlogAPIService.kt
│ │ ├── source
│ │ ├── AuthRemoteDataSourceImpl.kt
│ │ ├── AccountRemoteDataSourceImpl.kt
│ │ └── BlogRemoteDataSourceImpl.kt
│ │ └── koin
│ │ └── RemoteModule.kt
├── build.gradle.kts
└── proguard-rules.pro
├── features
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── dimen.xml
│ │ │ ├── styles.xml
│ │ │ └── colors.xml
│ │ ├── drawable
│ │ │ ├── default_image.png
│ │ │ ├── codingwithmitch_logo.png
│ │ │ ├── ic_home_white_24dp.xml
│ │ │ ├── ic_check_green_24dp.xml
│ │ │ ├── ic_filter_list_grey_24dp.xml
│ │ │ ├── ic_add_circle_outline_white_24dp.xml
│ │ │ ├── ic_edit_black_24dp.xml
│ │ │ ├── ic_account_circle_white_24dp.xml
│ │ │ ├── ic_mood_bad_black_24dp.xml
│ │ │ ├── red_button_drawable.xml
│ │ │ └── main_button_drawable.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── anim
│ │ │ ├── fade_out.xml
│ │ │ ├── fade_in.xml
│ │ │ ├── slide_out_left.xml
│ │ │ ├── slide_in_left.xml
│ │ │ ├── slide_in_right.xml
│ │ │ └── slide_out_right.xml
│ │ ├── color
│ │ │ └── bottom_nav_selector.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── menu
│ │ │ ├── publish_menu.xml
│ │ │ ├── edit_view_menu.xml
│ │ │ ├── update_menu.xml
│ │ │ ├── main_bottom_navigation_menu.xml
│ │ │ └── search_menu.xml
│ │ ├── navigation
│ │ │ ├── nav_create_blog.xml
│ │ │ ├── nav_auth.xml
│ │ │ ├── nav_blog.xml
│ │ │ └── nav_account.xml
│ │ ├── layout
│ │ │ ├── layout_no_more_results.xml
│ │ │ ├── fragment_blog.xml
│ │ │ ├── activity_splash.xml
│ │ │ ├── activity_auth.xml
│ │ │ ├── fragment_forget_password.xml
│ │ │ ├── fragment_update_account.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── layout_blog_list_item.xml
│ │ │ └── layout_blog_filter.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── com
│ │ │ └── mi
│ │ │ └── mvi
│ │ │ ├── model
│ │ │ ├── BaseModelView.kt
│ │ │ ├── UserView.kt
│ │ │ ├── BlogPostListView.kt
│ │ │ ├── TokenView.kt
│ │ │ └── BlogPostView.kt
│ │ │ ├── common
│ │ │ ├── DataStateChangeListener.kt
│ │ │ ├── UICommunicationListener.kt
│ │ │ ├── SessionManager.kt
│ │ │ └── ViewExtensions.kt
│ │ │ ├── features
│ │ │ ├── main
│ │ │ │ ├── blog
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ │ ├── BlogListItem.kt
│ │ │ │ │ │ ├── Pagination.kt
│ │ │ │ │ │ └── Getters.kt
│ │ │ │ │ ├── GenericViewHolder.kt
│ │ │ │ │ ├── BaseBlogFragment.kt
│ │ │ │ │ ├── BlogViewHolder.kt
│ │ │ │ │ └── Adapters.kt
│ │ │ │ ├── account
│ │ │ │ │ ├── BaseAccountFragment.kt
│ │ │ │ │ ├── ChangePasswordFragment.kt
│ │ │ │ │ ├── UpdateAccountFragment.kt
│ │ │ │ │ ├── AccountFragment.kt
│ │ │ │ │ └── AccountViewModel.kt
│ │ │ │ └── create_blog
│ │ │ │ │ └── CreateBlogViewModel.kt
│ │ │ └── auth
│ │ │ │ ├── RegisterFragment.kt
│ │ │ │ ├── LoginFragment.kt
│ │ │ │ ├── AuthViewModel.kt
│ │ │ │ ├── AuthActivity.kt
│ │ │ │ └── SplashActivity.kt
│ │ │ ├── events
│ │ │ ├── CreateBlogEventState.kt
│ │ │ ├── AuthEventState.kt
│ │ │ ├── BlogEventState.kt
│ │ │ └── AccountEventState.kt
│ │ │ ├── koin
│ │ │ ├── AuthModule.kt
│ │ │ └── MainModule.kt
│ │ │ ├── mapper
│ │ │ ├── Mapper.kt
│ │ │ ├── TokenMapper.kt
│ │ │ ├── UserMapper.kt
│ │ │ ├── BaseMapper.kt
│ │ │ └── BlogPostMapper.kt
│ │ │ ├── utils
│ │ │ └── Constants.kt
│ │ │ └── base
│ │ │ ├── BaseViewModel.kt
│ │ │ ├── BaseFragment.kt
│ │ │ └── BaseActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── app
├── .gitignore
├── build.gradle.kts
├── src
│ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── mi
│ │ │ └── mvi
│ │ │ ├── koin
│ │ │ └── KoinModules.kt
│ │ │ └── BaseApp.kt
│ │ └── AndroidManifest.xml
└── proguard-rules.pro
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── local.properties
├── settings.gradle.kts
├── LICENSE
├── gradle.properties
├── README.md
└── gradlew.bat
/cache/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/remote/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /build/
3 |
--------------------------------------------------------------------------------
/cache/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/repository/BaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.repository
2 |
3 | interface BaseRepository
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/features/src/main/res/values/dimen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 80dp
4 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/default_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/drawable/default_image.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/model/Token.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.model
2 |
3 | data class Token(
4 | var account_pk: Int? = -1,
5 | var token: String? = null
6 | )
7 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/codingwithmitch_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/drawable/codingwithmitch_logo.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/entity/TokenEntity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.entity
2 |
3 | data class TokenEntity(
4 | var account_pk: Int? = -1,
5 | var token: String? = null
6 | )
7 |
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/HEAD/features/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/entity/BaseEntity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.entity
2 |
3 | open class BaseEntity {
4 | var response: String? = null
5 | var errorMessage: String? = null
6 | }
7 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/model/BaseModelView.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.model
2 |
3 | open class BaseModelView {
4 | var response: String? = null
5 | var errorMessage: String? = null
6 | }
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/model/BaseModel.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.model
2 |
3 | open class BaseModel {
4 | var response: String? = null
5 | var errorMessage: String? = null
6 | }
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/model/BlogPostList.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.model
2 |
3 | data class BlogPostList(
4 | var results: MutableList?,
5 | var detail: String?
6 | )
7 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/entity/BlogPostListEntity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.entity
2 |
3 | data class BlogPostListEntity(
4 | var results: MutableList,
5 | var detail: String?
6 | )
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/viewstate/AccountViewState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.viewstate
2 |
3 | import com.mi.mvi.domain.model.User
4 |
5 | data class AccountViewState(
6 | var user: User? = null
7 | )
8 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/model/User.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.model
2 |
3 | data class User(
4 | var pk: Int,
5 | var email: String?,
6 | var username: String?,
7 | var token: String?
8 | ) : BaseModel()
9 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/model/UserView.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.model
2 |
3 | data class UserView(
4 | var pk: Int,
5 | var email: String?,
6 | var username: String?,
7 | var token: String?
8 | ) : BaseModelView()
9 |
--------------------------------------------------------------------------------
/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin
3 | }
4 |
5 | dependencies {
6 | implementation(LibraryDependency.RETROFIT)
7 | implementation(LibraryDependency.COROUTINES_CORE)
8 | implementation(LibraryDependency.KOIN)
9 | }
10 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/model/BlogPostListView.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.model
2 |
3 | import com.mi.mvi.domain.model.BlogPostView
4 |
5 | data class BlogPostListView(
6 | var results: MutableList?,
7 | var detail: String?
8 | )
9 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/common/DataStateChangeListener.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.common
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 |
5 | interface DataStateChangeListener {
6 | fun onDataStateChangeListener(dataState: DataState<*>?)
7 | }
8 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/entity/UserEntity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.entity
2 |
3 | data class UserEntity(
4 | var pk: Int,
5 | var email: String? = null,
6 | var username: String? = null,
7 | var token: String? = null
8 | ) : BaseEntity()
9 |
--------------------------------------------------------------------------------
/features/src/main/res/anim/fade_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Aug 13 08:44:12 EET 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
7 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/model/TokenView.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.android.parcel.Parcelize
5 |
6 | @Parcelize
7 | data class TokenView(
8 | var account_pk: Int? = -1,
9 | var token: String? = null
10 | ) : Parcelable
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | /build/
16 | /.idea/
17 |
--------------------------------------------------------------------------------
/features/src/main/res/color/bottom_nav_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/features/src/main/res/anim/fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin
3 | }
4 |
5 | dependencies {
6 | implementation(LibraryDependency.RETROFIT)
7 | implementation(LibraryDependency.COROUTINES_CORE)
8 | implementation(LibraryDependency.KOIN)
9 |
10 | // DOMAIN Module
11 | implementation(project(ModulesDependency.DOMAIN))
12 | }
13 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/viewmodel/BlogListItem.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog.viewmodel
2 |
3 | import com.mi.mvi.domain.model.BlogPostView
4 |
5 | sealed class BlogListItem {
6 | object NoMoreResult : BlogListItem()
7 | data class Item(val blogPostView: BlogPostView) : BlogListItem()
8 | }
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/features/src/main/res/anim/slide_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/features/src/main/res/anim/slide_in_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
--------------------------------------------------------------------------------
/features/src/main/res/anim/slide_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
--------------------------------------------------------------------------------
/features/src/main/res/anim/slide_out_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_home_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file must *NOT* be checked into Version Control Systems,
2 | # as it contains information specific to your local configuration.
3 | #
4 | # Location of the SDK. This is only used by Gradle.
5 | # For customization when using a Version Control System, please read the
6 | # header note.
7 | #Mon Feb 15 07:24:06 EET 2021
8 | sdk.dir=/data/workspace/android-sdk
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/model/BlogPost.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.model
2 |
3 | data class BlogPost(
4 | var pk: Int,
5 | var title: String? = null,
6 | var slug: String? = null,
7 | var body: String? = null,
8 | var image: String? = null,
9 | var date_updated: String? = null,
10 | var username: String? = null
11 | ) : BaseModel()
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/viewstate/CreateBlogViewState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.viewstate
2 |
3 | data class CreateBlogViewState(
4 | var newBlogField: NewBlogFields = NewBlogFields()
5 | )
6 |
7 | data class NewBlogFields(
8 | var newBlogTitle: String? = null,
9 | var newBlogBody: String? = null,
10 | var newImageUri: String? = null
11 | )
12 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_check_green_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/GenericViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog
2 |
3 | import android.view.View
4 | import com.mi.mvi.features.main.blog.viewmodel.BlogListItem.NoMoreResult
5 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder
6 |
7 | class GenericViewHolder(
8 | itemView: View
9 | ) : RecyclerViewHolder(itemView)
10 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/model/BaseRemote.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | open class BaseRemote {
7 | @SerializedName("response") @Expose var response: String? = null
8 | @SerializedName("error_message") @Expose var errorMessage: String? = null
9 | }
10 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/cache/TokenCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.cache
2 |
3 | import com.mi.mvi.data.entity.TokenEntity
4 |
5 | interface TokenCacheDataSource {
6 |
7 | suspend fun insert(tokenEntity: TokenEntity): Long
8 |
9 | suspend fun nullifyToken(pk: Int)
10 |
11 | suspend fun searchTokenByPk(pk: Int): TokenEntity?
12 | }
13 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/common/UICommunicationListener.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.common
2 |
3 | import com.mi.mvi.domain.datastate.StateMessage
4 |
5 | interface UICommunicationListener {
6 | fun onUIMessageReceived(stateMessage: StateMessage)
7 | fun hideSoftKeyboard()
8 | fun isStoragePermissionGranted(): Boolean
9 | fun displayLoading(isLoading: Boolean)
10 | }
11 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_filter_list_grey_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/features/src/main/res/menu/publish_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/events/CreateBlogEventState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.events
2 |
3 | import okhttp3.MultipartBody
4 |
5 | sealed class CreateBlogEventState {
6 |
7 | data class CreateNewBlogEvent(
8 | val title: String,
9 | val body: String,
10 | val image: MultipartBody.Part
11 | ) : CreateBlogEventState()
12 |
13 | object None : CreateBlogEventState()
14 | }
15 |
--------------------------------------------------------------------------------
/features/src/main/res/menu/edit_view_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/src/main/res/menu/update_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/model/RemoteBlogPostList.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class RemoteBlogPostList(
7 | @SerializedName("results")
8 | @Expose
9 | var results: MutableList,
10 | @SerializedName("detail")
11 | @Expose var detail: String
12 | )
13 |
--------------------------------------------------------------------------------
/remote/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin
3 | }
4 |
5 | dependencies {
6 | implementation(LibraryDependency.RETROFIT)
7 | implementation(LibraryDependency.RETROFIT_CONVERTER)
8 | implementation(LibraryDependency.RETROFIT_INTERCEPTOR)
9 | implementation(LibraryDependency.COLLECTION_KTX)
10 | implementation(LibraryDependency.KOIN)
11 |
12 | implementation(project(ModulesDependency.DATA))
13 | }
14 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/model/BlogPostView.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.model
2 |
3 | import com.mi.mvi.model.BaseModelView
4 |
5 | data class BlogPostView(
6 | var pk: Int,
7 | var title: String? = null,
8 | var slug: String? = null,
9 | var body: String? = null,
10 | var image: String? = null,
11 | var date_updated: String? = null,
12 | var username: String? = null
13 | ) : BaseModelView()
14 |
--------------------------------------------------------------------------------
/cache/build.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | plugins {
3 | id(GradlePluginId.ANDROID_LIB) // cache must be android lib for RoomDatabase
4 | id(GradlePluginId.BASE_GRADLE_PLUGIN)
5 | `kotlin-kapt`
6 | }
7 |
8 | dependencies {
9 | implementation(LibraryDependency.ROOM_RUNTIME)
10 | kapt(LibraryDependency.ROOM_COMPILER)
11 | implementation(LibraryDependency.ROOM_KTX)
12 | implementation(project(ModulesDependency.DATA))
13 | }
14 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_APP)
3 | id(GradlePluginId.BASE_GRADLE_PLUGIN)
4 | }
5 |
6 | dependencies {
7 | implementation(project(ModulesDependency.CACHE))
8 | implementation(project(ModulesDependency.REMOTE))
9 | implementation(project(ModulesDependency.DATA))
10 | implementation(project(ModulesDependency.DOMAIN))
11 | implementation(project(ModulesDependency.FEATURES))
12 | }
13 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/koin/AuthModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.koin
2 |
3 | import com.mi.mvi.features.auth.AuthViewModel
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.FlowPreview
6 | import org.koin.androidx.viewmodel.dsl.viewModel
7 | import org.koin.dsl.module
8 |
9 | @FlowPreview
10 | @ExperimentalCoroutinesApi
11 | val authModule = module {
12 | viewModel { AuthViewModel(get(), get(), get()) }
13 | }
14 |
--------------------------------------------------------------------------------
/features/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/mapper/EntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.mapper
2 |
3 | /**
4 | * Interface for model mappers. It provides helper methods that facilitate
5 | * retrieving of models from outer data source layers
6 | *
7 | * @param the remote model input type
8 | * @param the entity model output type
9 | */
10 | interface EntityMapper {
11 |
12 | fun mapFromRemote(type: REMOTE): ENTITY
13 | }
14 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/mapper/Mapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.mapper
2 |
3 | /**
4 | * Interface for model mappers. It provides helper methods that facilitate
5 | * retrieving of models from outer data source layers
6 | *
7 | * @param the remote model input type
8 | * @param the model return type
9 | */
10 | interface Mapper {
11 |
12 | fun mapFromView(type: Entity): Model
13 |
14 | fun mapToView(type: Model): Entity
15 | }
16 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/mapper/Mapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.mapper
2 |
3 | /**
4 | * Interface for model mappers. It provides helper methods that facilitate
5 | * retrieving of models from outer data source layers
6 | *
7 | * @param the remote model input type
8 | * @param the model return type
9 | */
10 | interface Mapper {
11 |
12 | fun mapFromEntity(type: Entity): Model
13 |
14 | fun mapToEntity(type: Model): Entity
15 | }
16 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/mapper/EntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.mapper
2 |
3 | /**
4 | * Interface for model mappers. It provides helper methods that facilitate
5 | * retrieving of models from outer data source layers
6 | *
7 | * @param the cached model input type
8 | * @param the model return type
9 | */
10 | interface EntityMapper {
11 | fun mapFromCached(type: CACHED): ENTITY
12 | fun mapToCached(type: ENTITY): CACHED
13 | }
14 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/remote/AuthRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.remote
2 |
3 | import com.mi.mvi.data.entity.UserEntity
4 |
5 | interface AuthRemoteDataSource {
6 |
7 | suspend fun login(
8 | email: String,
9 | password: String
10 | ): UserEntity
11 |
12 | suspend fun register(
13 | email: String,
14 | username: String,
15 | password: String,
16 | password2: String
17 | ): UserEntity
18 | }
19 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/model/RemoteUser.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class RemoteUser(
7 | @SerializedName("pk") @Expose var pk: Int,
8 | @SerializedName("email") @Expose var email: String? = null,
9 | @SerializedName("username") @Expose var username: String? = null,
10 | @SerializedName("token") @Expose var token: String? = null
11 | ) : BaseRemote()
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mi/mvi/koin/KoinModules.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.koin
2 |
3 | import com.mi.mvi.cache.koin.databaseModule
4 | import com.mi.mvi.cache.koin.sharedPrefsModule
5 | import com.mi.mvi.data.koin.dataModule
6 | import com.mi.mvi.domain.koin.useCaseModule
7 | import com.mi.mvi.remote.koin.remoteModule
8 |
9 | val koinModules = listOf(
10 | databaseModule,
11 | sharedPrefsModule,
12 | remoteModule,
13 | dataModule,
14 | useCaseModule,
15 | authModule,
16 | mainModule
17 | )
18 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_add_circle_outline_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/model/CachedUser.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "account")
8 | data class CachedUser(
9 | @PrimaryKey(autoGenerate = false)
10 | @ColumnInfo(name = "pk")
11 | var pk: Int,
12 |
13 | @ColumnInfo(name = "email")
14 | var email: String? = null,
15 |
16 | @ColumnInfo(name = "username")
17 | var username: String? = null
18 | )
19 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_edit_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/events/AuthEventState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.events
2 |
3 | sealed class AuthEventState {
4 |
5 | data class LoginEvent(
6 | val email: String,
7 | val password: String
8 | ) : AuthEventState()
9 |
10 | data class RegisterEvent(
11 | val email: String,
12 | val username: String,
13 | val password: String,
14 | val confirmPassword: String
15 | ) : AuthEventState()
16 |
17 | object CheckTokenEvent : AuthEventState()
18 |
19 | object None : AuthEventState()
20 | }
21 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/events/BlogEventState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.events
2 |
3 | import okhttp3.MultipartBody
4 |
5 | sealed class BlogEventState {
6 |
7 | object BlogSearchEvent : BlogEventState()
8 |
9 | object CheckAuthorBlogPostEvent : BlogEventState()
10 |
11 | object DeleteBlogPostEvent : BlogEventState()
12 |
13 | data class UpdateBlogPostEvent(
14 | val title: String,
15 | val body: String,
16 | val image: MultipartBody.Part?
17 | ) : BlogEventState()
18 |
19 | object None : BlogEventState()
20 | }
21 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/events/AccountEventState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.events
2 |
3 | sealed class AccountEventState {
4 |
5 | object GetAccountEvent : AccountEventState()
6 |
7 | data class UpdateAccountEvent(
8 | val email: String,
9 | val username: String
10 | ) : AccountEventState()
11 |
12 | data class ChangePasswordEvent(
13 | val currentPassword: String,
14 | val newPassword: String,
15 | val confirmNewPassword: String
16 | ) : AccountEventState()
17 |
18 | object None : AccountEventState()
19 | }
20 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_account_circle_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/koin/SharedPrefsModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.koin
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import org.koin.android.ext.koin.androidContext
6 | import org.koin.dsl.module
7 |
8 | const val APP_PREFERENCES: String = "com.mi.mvi.APP_PREFERENCES"
9 |
10 | val sharedPrefsModule = module {
11 | single {
12 | androidContext().getSharedPreferences(
13 | APP_PREFERENCES,
14 | Context.MODE_PRIVATE
15 | )
16 | }
17 | single { (get() as SharedPreferences).edit() }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/mi/mvi/BaseApp.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi
2 |
3 | import android.app.Application
4 | import com.mi.mvi.koin.koinModules
5 | import org.koin.android.ext.koin.androidContext
6 | import org.koin.android.ext.koin.androidLogger
7 | import org.koin.core.context.startKoin
8 | import org.koin.core.logger.Level
9 |
10 | class BaseApp : Application() {
11 | override fun onCreate() {
12 | super.onCreate()
13 | startKoin {
14 | androidLogger(Level.DEBUG)
15 | androidContext(this@BaseApp)
16 | modules(koinModules)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/cache/AccountCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.cache
2 |
3 | import com.mi.mvi.data.entity.UserEntity
4 |
5 | interface AccountCacheDataSource {
6 |
7 | suspend fun insertOrIgnore(userEntity: UserEntity): Long
8 |
9 | suspend fun searchByPk(pk: Int): UserEntity?
10 |
11 | suspend fun searchByEmail(email: String): UserEntity?
12 |
13 | suspend fun updateAccountProperties(pk: Int, email: String?, username: String?)
14 |
15 | fun getLoggedInEmail(): String?
16 |
17 | fun saveLoggedInEmail(email: String?)
18 | }
19 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/account/BaseAccountFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.account
2 |
3 | import com.mi.mvi.base.BaseFragment
4 | import com.mi.mvi.mapper.UserMapper
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.FlowPreview
7 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel
8 |
9 | @FlowPreview
10 | @ExperimentalCoroutinesApi
11 | abstract class BaseAccountFragment(contentLayoutId: Int) : BaseFragment(contentLayoutId) {
12 |
13 | val userMapper = UserMapper()
14 | val viewModel: AccountViewModel by sharedViewModel()
15 | }
16 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/repository/CreateBlogRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.repository
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.viewstate.CreateBlogViewState
6 | import kotlinx.coroutines.flow.Flow
7 | import okhttp3.MultipartBody
8 | import okhttp3.RequestBody
9 |
10 | interface CreateBlogRepository : BaseRepository {
11 |
12 | fun createNewBlogPost(
13 | token: Token,
14 | title: RequestBody,
15 | body: RequestBody,
16 | image: MultipartBody.Part
17 | ): Flow>
18 | }
19 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/blogs/FiltrationUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.blogs
2 |
3 | import com.mi.mvi.domain.repository.BlogRepository
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 |
6 | @ExperimentalCoroutinesApi
7 | class FiltrationUseCase(private val repository: BlogRepository) {
8 |
9 | fun saveFilterOptions(filter: String, order: String) {
10 | repository.saveFilterOptions(filter, order)
11 | }
12 |
13 | fun getFilter(): String? {
14 | return repository.getFilter()
15 | }
16 |
17 | fun getOrder(): String? {
18 | return repository.getOrder()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/features/src/main/res/navigation/nav_create_blog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/layout_no_more_results.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
15 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/mapper/TokenMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.mapper
2 |
3 | import com.mi.mvi.domain.model.Token
4 | import com.mi.mvi.model.TokenView
5 | import com.mi.mvi.model.UserView
6 |
7 | /**
8 | * Map a [UserView] to and from a [UserEntity] instance when data is moving between
9 | * this later and the Data layer
10 | */
11 | open class TokenMapper : Mapper {
12 |
13 | override fun mapFromView(type: TokenView): Token {
14 | return Token(type.account_pk, type.token)
15 | }
16 |
17 | override fun mapToView(type: Token): TokenView {
18 | return TokenView(type.account_pk, type.token)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/BaseBlogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog
2 |
3 | import com.mi.mvi.base.BaseFragment
4 | import com.mi.mvi.features.main.blog.viewmodel.BlogViewModel
5 | import com.mi.mvi.mapper.BlogPostMapper
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.FlowPreview
8 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel
9 |
10 | @FlowPreview
11 | @ExperimentalCoroutinesApi
12 | abstract class BaseBlogFragment(contentLayoutId: Int) : BaseFragment(contentLayoutId) {
13 | val blogPostMapper = BlogPostMapper()
14 | val viewModel: BlogViewModel by sharedViewModel()
15 | }
16 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/db/AuthTokenDao.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.mi.mvi.cache.model.CachedToken
8 |
9 | @Dao
10 | interface AuthTokenDao {
11 |
12 | @Insert(onConflict = OnConflictStrategy.REPLACE)
13 | suspend fun insert(cachedToken: CachedToken): Long
14 |
15 | @Query("UPDATE auth_token SET token = null WHERE account_pk = :pk")
16 | suspend fun nullifyToken(pk: Int)
17 |
18 | @Query("SELECT * FROM auth_token WHERE account_pk = :pk")
19 | suspend fun searchTokenByPk(pk: Int): CachedToken?
20 | }
21 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/auth/CheckTokenUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.auth
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.repository.AuthRepository
5 | import com.mi.mvi.domain.viewstate.AuthViewState
6 | import kotlinx.coroutines.Dispatchers.IO
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flowOn
10 |
11 | @ExperimentalCoroutinesApi
12 | class CheckTokenUseCase(private val repository: AuthRepository) {
13 |
14 | fun invoke(): Flow> {
15 | return repository.checkPreviousAuthUser().flowOn(IO)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/db/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.mi.mvi.cache.model.CachedBlogPost
6 | import com.mi.mvi.cache.model.CachedToken
7 | import com.mi.mvi.cache.model.CachedUser
8 |
9 | @Database(entities = [CachedUser::class, CachedToken::class, CachedBlogPost::class], version = 1)
10 | abstract class AppDatabase : RoomDatabase() {
11 |
12 | abstract fun getAuthTokenDao(): AuthTokenDao
13 | abstract fun getAccountDao(): AccountDao
14 | abstract fun getBlogPostDao(): BlogPostDao
15 |
16 | companion object {
17 | const val DATABASE_NAME = "app_db"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.utils
2 |
3 | import com.mi.mvi.domain.Constants.Companion.INVALID_PAGE
4 |
5 | class Constants {
6 |
7 | companion object {
8 |
9 | // Shared Preference Keys
10 |
11 | // -----------------------------UI-----------------------
12 | const val GALLERY_REQUEST_CODE = 201
13 | const val PERMISSION_REQUEST_READ_STORAGE: Int = 301
14 |
15 | fun isPaginationDone(errorResponse: String?): Boolean {
16 | // if error response = '{"detail":"Invalid page."}' then pagination is finished
17 | return errorResponse?.contains(INVALID_PAGE) ?: false
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/features/src/main/res/menu/main_bottom_navigation_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/auth/LoginUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.auth
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.repository.AuthRepository
5 | import com.mi.mvi.domain.viewstate.AuthViewState
6 | import kotlinx.coroutines.Dispatchers.IO
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flowOn
10 |
11 | @ExperimentalCoroutinesApi
12 | class LoginUseCase(private val repository: AuthRepository) {
13 |
14 | fun invoke(email: String, password: String): Flow> {
15 | return repository.login(email, password).flowOn(IO)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/mapper/TokenMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.mapper
2 |
3 | import com.mi.mvi.data.entity.TokenEntity
4 | import com.mi.mvi.data.entity.UserEntity
5 | import com.mi.mvi.domain.model.Token
6 | import com.mi.mvi.domain.model.User
7 |
8 | /**
9 | * Map a [User] to and from a [UserEntity] instance when data is moving between
10 | * this later and the Data layer
11 | */
12 | open class TokenMapper : Mapper {
13 |
14 | override fun mapFromEntity(type: TokenEntity): Token {
15 | return Token(type.account_pk, type.token)
16 | }
17 |
18 | override fun mapToEntity(type: Token): TokenEntity {
19 | return TokenEntity(type.account_pk, type.token)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/model/CachedToken.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.ForeignKey
6 | import androidx.room.ForeignKey.CASCADE
7 | import androidx.room.PrimaryKey
8 |
9 | @Entity(
10 | tableName = "auth_token",
11 | foreignKeys = [ForeignKey(
12 | entity = CachedUser::class,
13 | parentColumns = ["pk"],
14 | childColumns = ["account_pk"],
15 | onDelete = CASCADE
16 | )]
17 | )
18 | data class CachedToken(
19 |
20 | @PrimaryKey
21 | @ColumnInfo(name = "account_pk")
22 | var account_pk: Int? = -1,
23 |
24 | @ColumnInfo(name = "token")
25 | var token: String? = null
26 | )
27 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/remote/AccountRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.remote
2 |
3 | import com.mi.mvi.data.entity.BaseEntity
4 | import com.mi.mvi.data.entity.UserEntity
5 |
6 | interface AccountRemoteDataSource {
7 |
8 | suspend fun getAccountProperties(
9 | authorization: String
10 | ): UserEntity
11 |
12 | suspend fun updateAccountProperties(
13 | authorization: String,
14 | email: String?,
15 | username: String?
16 | ): BaseEntity
17 |
18 | suspend fun changePassword(
19 | authorization: String,
20 | currentPassword: String,
21 | newPassword: String,
22 | confirmNewPassword: String
23 | ): BaseEntity
24 | }
25 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/repository/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.repository
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.viewstate.AuthViewState
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface AuthRepository : BaseRepository {
8 |
9 | fun login(
10 | email: String,
11 | password: String
12 | ): Flow>
13 |
14 | fun register(
15 | email: String,
16 | username: String,
17 | password: String,
18 | confirmPassword: String
19 | ): Flow>
20 |
21 | fun checkPreviousAuthUser(): Flow>
22 |
23 | suspend fun nullifyToken(pk: Int)
24 | }
25 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/ic_mood_bad_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/mapper/BaseEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.mapper
2 |
3 | import com.mi.mvi.data.entity.BaseEntity
4 | import com.mi.mvi.remote.model.BaseRemote
5 |
6 | /**
7 | * Map a [BaseRemote] to and from a [BaseEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | open class BaseEntityMapper : EntityMapper {
11 |
12 | /**
13 | * Map an instance of a [BaseRemote] to a [BaseEntity] model
14 | */
15 | override fun mapFromRemote(type: BaseRemote): BaseEntity {
16 | val baseEntity = BaseEntity()
17 | baseEntity.response = type.response
18 | baseEntity.errorMessage = type.errorMessage
19 | return baseEntity
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/account/GetAccountUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.account
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.repository.AccountRepository
6 | import com.mi.mvi.domain.viewstate.AccountViewState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOn
11 |
12 | @ExperimentalCoroutinesApi
13 | class GetAccountUseCase(private val repository: AccountRepository) {
14 |
15 | fun invoke(tokenEntity: Token): Flow> {
16 | return repository.getAccountProperties(tokenEntity).flowOn(Dispatchers.IO)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/service/AuthAPIService.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.service
2 |
3 | import com.mi.mvi.remote.model.RemoteUser
4 | import retrofit2.http.Field
5 | import retrofit2.http.FormUrlEncoded
6 | import retrofit2.http.POST
7 |
8 | interface AuthAPIService {
9 |
10 | @POST("account/login")
11 | @FormUrlEncoded
12 | suspend fun login(
13 | @Field("username") email: String,
14 | @Field("password") password: String
15 | ): RemoteUser
16 |
17 | @POST("account/register")
18 | @FormUrlEncoded
19 | suspend fun register(
20 | @Field("email") email: String,
21 | @Field("username") username: String,
22 | @Field("password") password: String,
23 | @Field("password2") password2: String
24 | ): RemoteUser
25 | }
26 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/datastate/StateResource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.datastate
2 |
3 | data class StateMessage(
4 | val message: String?,
5 | val uiComponentType: UIComponentType,
6 | val messageType: MessageType
7 | )
8 |
9 | sealed class UIComponentType {
10 | object TOAST : UIComponentType()
11 | object DIALOG : UIComponentType()
12 | class AreYouSureDialog(
13 | val callBack: AreYouSureCallBack
14 | ) : UIComponentType()
15 |
16 | object NONE : UIComponentType()
17 | }
18 |
19 | sealed class MessageType {
20 | object SUCCESS : MessageType()
21 | object ERROR : MessageType()
22 | object INFO : MessageType()
23 | object NONE : MessageType()
24 | }
25 |
26 | interface AreYouSureCallBack {
27 | fun proceed()
28 | fun cancel()
29 | }
30 |
--------------------------------------------------------------------------------
/features/src/main/res/menu/search_menu.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/cache/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/domain/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/repository/AccountRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.repository
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.model.User
6 | import com.mi.mvi.domain.viewstate.AccountViewState
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | interface AccountRepository : BaseRepository {
10 |
11 | fun getAccountProperties(token: Token):
12 | Flow>
13 |
14 | fun updateAccountProperties(
15 | token: Token,
16 | user: User
17 | ): Flow>
18 |
19 | fun changePassword(
20 | token: Token,
21 | currentPassword: String,
22 | newPassword: String,
23 | confirmNewPassword: String
24 | ): Flow>
25 | }
26 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/auth/RegisterUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.auth
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.repository.AuthRepository
5 | import com.mi.mvi.domain.viewstate.AuthViewState
6 | import kotlinx.coroutines.Dispatchers.IO
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flowOn
10 |
11 | @ExperimentalCoroutinesApi
12 | class RegisterUseCase(private val repository: AuthRepository) {
13 | fun invoke(
14 | email: String,
15 | username: String,
16 | password: String,
17 | confirmPassword: String
18 | ): Flow> {
19 | return repository.register(email, username, password, confirmPassword).flowOn(IO)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/blogs/IsAuthorBlogPostUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.blogs
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.repository.BlogRepository
6 | import com.mi.mvi.domain.viewstate.BlogViewState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOn
11 |
12 | @ExperimentalCoroutinesApi
13 | class IsAuthorBlogPostUseCase(private val repository: BlogRepository) {
14 |
15 | fun invoke(
16 | token: Token,
17 | slug: String
18 | ): Flow> {
19 | return repository.isAuthorOfBlogPosts(token, slug)
20 | .flowOn(Dispatchers.IO)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/features/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/mapper/UserMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.mapper
2 |
3 | import com.mi.mvi.domain.model.User
4 | import com.mi.mvi.model.TokenView
5 | import com.mi.mvi.model.UserView
6 |
7 | /**
8 | * Map a [TokenView] to and from a [TokenEntity] instance when data is moving between
9 | * this later and the Data layer
10 | */
11 | open class UserMapper : Mapper {
12 | override fun mapToView(type: User): UserView {
13 | return UserView(
14 | type.pk,
15 | type.email,
16 | type.username,
17 | type.token
18 | )
19 | }
20 |
21 | override fun mapFromView(type: UserView): User {
22 | return User(
23 | type.pk,
24 | type.email,
25 | type.username,
26 | type.token
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/remote/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/db/AccountDao.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.mi.mvi.cache.model.CachedUser
8 |
9 | @Dao
10 | interface AccountDao {
11 |
12 | @Insert(onConflict = OnConflictStrategy.IGNORE)
13 | suspend fun insertOrIgnore(cachedUser: CachedUser): Long
14 |
15 | @Query("SELECT * FROM account WHERE pk = :pk")
16 | suspend fun searchByPk(pk: Int): CachedUser?
17 |
18 | @Query("SELECT * FROM account WHERE email = :email")
19 | suspend fun searchByEmail(email: String): CachedUser?
20 |
21 | @Query("Update account Set email = :email, username = :username WHERE pk = :pk ")
22 | suspend fun updateAccountProperties(pk: Int, email: String?, username: String?)
23 | }
24 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | resolutionStrategy {
3 | eachPlugin {
4 | if (requested.id.id == "com.android.library") {
5 | useModule("com.android.tools.build:gradle:${requested.version}")
6 | }
7 | if (requested.id.id == "com.android.application") {
8 | useModule("com.android.tools.build:gradle:${requested.version}")
9 | }
10 | }
11 | }
12 | repositories {
13 | gradlePluginPortal()
14 | google()
15 | mavenCentral()
16 | }
17 | }
18 |
19 | rootProject.name = ("Clean Architecture With MVI")
20 |
21 | // this gradle version has an issue with settings kts file to read from constants
22 | include(
23 | "app",
24 | "features",
25 | "domain",
26 | "data",
27 | "cache",
28 | "remote"
29 | )
30 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/datastate/DataState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.datastate
2 |
3 | sealed class DataState(
4 | var loading: Boolean,
5 | var data: T? = null,
6 | var stateMessage: StateMessage? = null
7 | ) {
8 | class LOADING(
9 | isLoading: Boolean,
10 | cachedData: T? = null
11 | ) : DataState(
12 | loading = isLoading,
13 | data = cachedData
14 | )
15 |
16 | class SUCCESS(
17 | data: T? = null,
18 | stateMessage: StateMessage? = null
19 | ) : DataState(
20 | loading = false,
21 | data = data,
22 | stateMessage = stateMessage
23 | )
24 |
25 | class ERROR(
26 | stateMessage: StateMessage
27 | ) : DataState(
28 | loading = false,
29 | data = null,
30 | stateMessage = stateMessage
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/mapper/UserEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.mapper
2 |
3 | import com.mi.mvi.cache.model.CachedUser
4 | import com.mi.mvi.data.entity.UserEntity
5 |
6 | /**
7 | * Map a [CachedUser] instance to and from a [UserEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | class UserEntityMapper : EntityMapper {
11 |
12 | /**
13 | * Map a [CachedUser] instance to a [UserEntity] instance
14 | */
15 | override fun mapFromCached(type: CachedUser): UserEntity {
16 | return UserEntity(type.pk, type.email, type.username)
17 | }
18 |
19 | /**
20 | * Map a [UserEntity] instance to a [CachedUser] instance
21 | */
22 | override fun mapToCached(type: UserEntity): CachedUser {
23 | return CachedUser(type.pk, type.email, type.username)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/blogs/DeleteBlogPostUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.blogs
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.BlogPost
5 | import com.mi.mvi.domain.model.Token
6 | import com.mi.mvi.domain.repository.BlogRepository
7 | import com.mi.mvi.domain.viewstate.BlogViewState
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.flowOn
12 |
13 | @ExperimentalCoroutinesApi
14 | class DeleteBlogPostUseCase(private val repository: BlogRepository) {
15 |
16 | fun invoke(
17 | token: Token,
18 | blogPost: BlogPost
19 | ): Flow> {
20 | return repository.deleteBlogPost(token, blogPost)
21 | .flowOn(Dispatchers.IO)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/mapper/TokenEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.mapper
2 |
3 | import com.mi.mvi.cache.model.CachedToken
4 | import com.mi.mvi.data.entity.TokenEntity
5 |
6 | /**
7 | * Map a [CachedToken] instance to and from a [TokenEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | class TokenEntityMapper : EntityMapper {
11 |
12 | /**
13 | * Map a [CachedToken] instance to a [TokenEntity] instance
14 | */
15 | override fun mapFromCached(type: CachedToken): TokenEntity {
16 | return TokenEntity(type.account_pk, type.token)
17 | }
18 |
19 | /**
20 | * Map a [TokenEntity] instance to a [CachedToken] instance
21 | */
22 | override fun mapToCached(type: TokenEntity): CachedToken {
23 | return CachedToken(type.account_pk, type.token)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/mapper/BaseMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.mapper
2 |
3 | import com.mi.mvi.data.entity.BaseEntity
4 | import com.mi.mvi.domain.model.BaseModel
5 |
6 | /**
7 | * Map a [BaseEntity] to and from a [BaseModel] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | open class BaseMapper : Mapper {
11 |
12 | override fun mapFromEntity(type: BaseEntity): BaseModel {
13 | val baseModel = BaseModel()
14 | baseModel.response = type.response
15 | baseModel.errorMessage = type.errorMessage
16 | return baseModel
17 | }
18 |
19 | override fun mapToEntity(type: BaseModel): BaseEntity {
20 | val baseEntity = BaseEntity()
21 | baseEntity.response = type.response
22 | baseEntity.errorMessage = type.errorMessage
23 | return baseEntity
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/account/UpdateAccountUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.account
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.model.User
6 | import com.mi.mvi.domain.repository.AccountRepository
7 | import com.mi.mvi.domain.viewstate.AccountViewState
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.flowOn
12 |
13 | @ExperimentalCoroutinesApi
14 | class UpdateAccountUseCase(private val repository: AccountRepository) {
15 |
16 | fun invoke(
17 | token: Token,
18 | user: User
19 | ): Flow> {
20 | return repository.updateAccountProperties(token, user)
21 | .flowOn(Dispatchers.IO)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/features/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #fff
6 | #7d7d7d
7 | #FF4081
8 |
9 |
10 | #0094DE
11 | #9FDAF7
12 | #0000EE
13 |
14 |
15 | #e22b2b
16 | #fc8b8d
17 | #f2f2f2
18 | #c4c4c4
19 | #a8a8a8
20 | #7d7d7d
21 | #5c5c5c
22 |
23 | #EEEEEE
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/mapper/BaseMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.mapper
2 |
3 | import com.mi.mvi.domain.model.BaseModel
4 | import com.mi.mvi.model.BaseModelView
5 |
6 | /**
7 | * Map a [BaseEntity] to and from a [BaseModelView] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | open class BaseMapper : Mapper {
11 |
12 | override fun mapFromView(type: BaseModelView): BaseModel {
13 | val baseModel = BaseModel()
14 | baseModel.response = type.response
15 | baseModel.errorMessage = type.errorMessage
16 | return baseModel
17 | }
18 |
19 | override fun mapToView(type: BaseModel): BaseModelView {
20 | val baseModelView = BaseModelView()
21 | baseModelView.response = type.response
22 | baseModelView.errorMessage = type.errorMessage
23 | return baseModelView
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/blogs/SearchBlogUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.blogs
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.repository.BlogRepository
6 | import com.mi.mvi.domain.viewstate.BlogViewState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOn
11 |
12 | @ExperimentalCoroutinesApi
13 | class SearchBlogUseCase(private val repository: BlogRepository) {
14 |
15 | fun invoke(
16 | token: Token,
17 | query: String,
18 | filterAndOrder: String,
19 | page: Int
20 | ): Flow> {
21 | return repository.searchBlogPosts(token, query, filterAndOrder, page)
22 | .flowOn(Dispatchers.IO)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/mapper/UserMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.mapper
2 |
3 | import com.mi.mvi.data.entity.TokenEntity
4 | import com.mi.mvi.data.entity.UserEntity
5 | import com.mi.mvi.domain.model.Token
6 | import com.mi.mvi.domain.model.User
7 |
8 | /**
9 | * Map a [Token] to and from a [TokenEntity] instance when data is moving between
10 | * this later and the Data layer
11 | */
12 | open class UserMapper : Mapper {
13 | override fun mapFromEntity(type: UserEntity): User {
14 | return User(
15 | type.pk,
16 | type.email,
17 | type.username,
18 | type.token
19 | )
20 | }
21 |
22 | override fun mapToEntity(type: User): UserEntity {
23 | return UserEntity(
24 | type.pk,
25 | type.email,
26 | type.username,
27 | type.token
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/account/ChangePasswordUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.account
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.repository.AccountRepository
6 | import com.mi.mvi.domain.viewstate.AccountViewState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOn
11 |
12 | @ExperimentalCoroutinesApi
13 | class ChangePasswordUseCase(private val repository: AccountRepository) {
14 |
15 | fun invoke(
16 | token: Token,
17 | currentPassword: String,
18 | newPassword: String,
19 | confirmNewPassword: String
20 | ): Flow> {
21 | return repository.changePassword(token, currentPassword, newPassword, confirmNewPassword)
22 | .flowOn(Dispatchers.IO)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/entity/BlogPostEntity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.entity
2 |
3 | import java.text.SimpleDateFormat
4 | import java.util.*
5 |
6 | data class BlogPostEntity(
7 | var pk: Int,
8 | var title: String? = null,
9 | var slug: String? = null,
10 | var body: String? = null,
11 | var image: String? = null,
12 | var date_updated: String? = null,
13 | var username: String? = null
14 | ) : BaseEntity() {
15 | // dates from server look like this: "2019-07-23T03:28:01.406944Z"
16 | fun getDateAsLong(): Long {
17 |
18 | date_updated?.let {
19 | val stringDate =
20 | it.removeRange(it.indexOf("T") until it.length)
21 | val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
22 | try {
23 | return sdf.parse(stringDate).time
24 | } catch (e: Exception) {
25 | throw Exception(e)
26 | }
27 | } ?: return 0L
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/mapper/BlogPostEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.mapper
2 |
3 | import com.mi.mvi.data.entity.BlogPostEntity
4 | import com.mi.mvi.remote.model.RemoteBlogPost
5 |
6 | /**
7 | * Map a [RemoteBlogPost] to and from a [BlogPostEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | open class BlogPostEntityMapper : EntityMapper {
11 |
12 | /**
13 | * Map an instance of a [RemoteBlogPost] to a [BlogPostEntity] model
14 | */
15 | override fun mapFromRemote(type: RemoteBlogPost): BlogPostEntity {
16 | val blogPostEntity = BlogPostEntity(
17 | type.pk,
18 | type.title,
19 | type.slug,
20 | type.body,
21 | type.image,
22 | type.date_updated,
23 | type.username
24 | )
25 | blogPostEntity.response = type.response
26 | return blogPostEntity
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/mapper/UserEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.mapper
2 |
3 | import com.mi.mvi.data.entity.BaseEntity
4 | import com.mi.mvi.data.entity.UserEntity
5 | import com.mi.mvi.remote.model.BaseRemote
6 | import com.mi.mvi.remote.model.RemoteUser
7 |
8 | /**
9 | * Map a [BaseRemote] to and from a [BaseEntity] instance when data is moving between
10 | * this later and the Data layer
11 | */
12 | open class UserEntityMapper : EntityMapper {
13 |
14 | /**
15 | * Map an instance of a [RemoteUser] to a [BaseEntityMapper] model
16 | */
17 | override fun mapFromRemote(type: RemoteUser): UserEntity {
18 | val userEntity = UserEntity(
19 | type.pk,
20 | type.email,
21 | type.username,
22 | type.token
23 | )
24 | userEntity.response = type.response
25 | userEntity.errorMessage = type.errorMessage
26 | return userEntity
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/blogs/UpdateBlogPostUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.blogs
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.repository.BlogRepository
6 | import com.mi.mvi.domain.viewstate.BlogViewState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import okhttp3.MultipartBody
12 | import okhttp3.RequestBody
13 |
14 | @ExperimentalCoroutinesApi
15 | class UpdateBlogPostUseCase(private val repository: BlogRepository) {
16 |
17 | fun invoke(
18 | token: Token,
19 | slug: String,
20 | title: RequestBody,
21 | body: RequestBody,
22 | image: MultipartBody.Part?
23 | ): Flow> {
24 | return repository.updateBlogPost(token, slug, title, body, image)
25 | .flowOn(Dispatchers.IO)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/source/TokenCacheDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.source
2 |
3 | import com.mi.mvi.cache.db.AuthTokenDao
4 | import com.mi.mvi.cache.mapper.TokenEntityMapper
5 | import com.mi.mvi.data.datasource.cache.TokenCacheDataSource
6 | import com.mi.mvi.data.entity.TokenEntity
7 |
8 | class TokenCacheDataSourceImpl(
9 | private val authTokenDao: AuthTokenDao,
10 | private val tokenEntityMapper: TokenEntityMapper
11 | ) : TokenCacheDataSource {
12 |
13 | override suspend fun insert(tokenEntity: TokenEntity): Long {
14 | return authTokenDao.insert(tokenEntityMapper.mapToCached(tokenEntity))
15 | }
16 |
17 | override suspend fun nullifyToken(pk: Int) {
18 | return authTokenDao.nullifyToken(pk)
19 | }
20 |
21 | override suspend fun searchTokenByPk(pk: Int): TokenEntity? {
22 | authTokenDao.searchTokenByPk(pk)?.let { cachedToken ->
23 | return tokenEntityMapper.mapFromCached(cachedToken)
24 | } ?: return null
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/mapper/BlogPostMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.mapper
2 |
3 | import com.mi.mvi.domain.model.BlogPost
4 | import com.mi.mvi.domain.model.BlogPostView
5 |
6 | /**
7 | * Map a [BlogPostView] to and from a [BlogPostEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | open class BlogPostMapper : Mapper {
11 | override fun mapFromView(type: BlogPostView): BlogPost {
12 | return BlogPost(
13 | type.pk,
14 | type.title,
15 | type.slug,
16 | type.body,
17 | type.image,
18 | type.date_updated,
19 | type.username
20 | )
21 | }
22 |
23 | override fun mapToView(type: BlogPost): BlogPostView {
24 | return BlogPostView(
25 | type.pk,
26 | type.title,
27 | type.slug,
28 | type.body,
29 | type.image,
30 | type.date_updated,
31 | type.username
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/usecase/blogs/CreateBlogUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.usecase.blogs
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.Token
5 | import com.mi.mvi.domain.repository.CreateBlogRepository
6 | import com.mi.mvi.domain.viewstate.CreateBlogViewState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import okhttp3.MultipartBody
12 | import okhttp3.RequestBody
13 |
14 | @ExperimentalCoroutinesApi
15 | class CreateBlogUseCase(private val repository: CreateBlogRepository) {
16 | fun invoke(
17 | tokenEntity: Token,
18 | title: RequestBody,
19 | body: RequestBody,
20 | image: MultipartBody.Part
21 | ): Flow> {
22 | return repository.createNewBlogPost(
23 | tokenEntity,
24 | title,
25 | body,
26 | image
27 | ).flowOn(Dispatchers.IO)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/service/AccountAPIService.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.service
2 |
3 | import com.mi.mvi.remote.model.BaseRemote
4 | import com.mi.mvi.remote.model.RemoteUser
5 | import retrofit2.http.*
6 |
7 | interface AccountAPIService {
8 | @GET("account/properties")
9 | suspend fun getAccountProperties(
10 | @Header("Authorization") authorization: String
11 | ): RemoteUser
12 |
13 | @PUT("account/properties/update")
14 | @FormUrlEncoded
15 | suspend fun updateAccountProperties(
16 | @Header("Authorization") authorization: String,
17 | @Field("email") email: String?,
18 | @Field("username") username: String?
19 | ): BaseRemote
20 |
21 | @PUT("account/change_password/")
22 | @FormUrlEncoded
23 | suspend fun changePassword(
24 | @Header("Authorization") authorization: String,
25 | @Field("old_password") currentPassword: String,
26 | @Field("new_password") newPassword: String,
27 | @Field("confirm_new_password") confirmNewPassword: String
28 | ): BaseRemote
29 | }
30 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/remote/BlogRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.remote
2 |
3 | import com.mi.mvi.data.entity.*
4 | import okhttp3.MultipartBody
5 | import okhttp3.RequestBody
6 |
7 | interface BlogRemoteDataSource {
8 |
9 | suspend fun searchListBlogPosts(
10 | authorization: String,
11 | query: String,
12 | ordering: String,
13 | page: Int
14 | ): BlogPostListEntity
15 |
16 | suspend fun isAuthorOfBlogPost(
17 | authorization: String,
18 | slug: String
19 | ): BaseEntity
20 |
21 | suspend fun deleteBlogPost(
22 | authorization: String,
23 | slug: String?
24 | ): BaseEntity
25 |
26 | suspend fun updateBlog(
27 | authorization: String,
28 | slug: String,
29 | title: RequestBody,
30 | body: RequestBody,
31 | image: MultipartBody.Part?
32 | ): BlogPostEntity
33 |
34 | suspend fun createBlog(
35 | authorization: String,
36 | title: RequestBody,
37 | body: RequestBody,
38 | image: MultipartBody.Part?
39 | ): BlogPostEntity
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Mohamed Ibrahim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/viewstate/BlogViewState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.viewstate
2 |
3 | import com.mi.mvi.domain.model.BlogPost
4 |
5 | const val BLOG_VIEW_STATE_BUNDLE_KEY = "BLOG_VIEW_STATE_BUNDLE_KEY"
6 |
7 | data class BlogViewState(
8 | // BlogFragment vars
9 | var blogFields: BlogFields = BlogFields(),
10 |
11 | // ViewBlogFragment vars
12 | var viewBlogFields: ViewBlogFields = ViewBlogFields(),
13 |
14 | // UpdateBlogFragment vars
15 | var updatedBlogFields: UpdatedBlogFields = UpdatedBlogFields()
16 | )
17 |
18 | data class BlogFields(
19 | var blogList: MutableList? = null,
20 | var searchQuery: String? = null,
21 | var page: Int? = null,
22 | var isQueryExhausted: Boolean? = null,
23 | var filter: String? = null,
24 | var order: String? = null
25 | )
26 |
27 | data class ViewBlogFields(
28 | var blogPostEntity: BlogPost? = null,
29 | var isAuthor: Boolean? = null
30 | )
31 |
32 | data class UpdatedBlogFields(
33 | var updatedBlogTitle: String? = null,
34 | var updatedBlogBody: String? = null,
35 | var updatedImageUri: String? = null
36 | )
37 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/mapper/BlogPostMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.mapper
2 |
3 | import com.mi.mvi.data.entity.BlogPostEntity
4 | import com.mi.mvi.domain.model.BlogPost
5 |
6 | /**
7 | * Map a [BlogPost] to and from a [BlogPostEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | open class BlogPostMapper : Mapper {
11 | override fun mapFromEntity(type: BlogPostEntity): BlogPost {
12 | return BlogPost(
13 | type.pk,
14 | type.title,
15 | type.slug,
16 | type.body,
17 | type.image,
18 | type.date_updated,
19 | type.username
20 | )
21 | }
22 |
23 | override fun mapToEntity(type: BlogPost): BlogPostEntity {
24 | val blogPostEntity = BlogPostEntity(
25 | type.pk,
26 | type.title,
27 | type.slug,
28 | type.body,
29 | type.image,
30 | type.date_updated,
31 | type.username
32 | )
33 | blogPostEntity.response = type.response
34 | return blogPostEntity
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/BlogViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog
2 |
3 | import android.view.View
4 | import com.bumptech.glide.Glide
5 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
6 | import com.mi.mvi.R
7 | import com.mi.mvi.features.main.blog.viewmodel.BlogListItem
8 | import kotlinx.android.synthetic.main.layout_blog_list_item.view.*
9 | import me.ibrahimyilmaz.kiel.core.RecyclerViewHolder
10 |
11 | class BlogViewHolder(
12 | itemView: View
13 | ) : RecyclerViewHolder(itemView) {
14 |
15 | override fun bind(position: Int, item: BlogListItem.Item) {
16 | val blogPostView = item.blogPostView
17 | super.bind(position, item)
18 | Glide.with(itemView)
19 | .load(blogPostView.image)
20 | .placeholder(R.drawable.default_image)
21 | .transition(withCrossFade())
22 | .into(itemView.imgBlog)
23 |
24 | itemView.tvBlogTitle.text = blogPostView.title
25 | itemView.tvBlogAuthor.text = blogPostView.username
26 | itemView.tvBlogDate.text = blogPostView.date_updated
27 | }
28 | }
--------------------------------------------------------------------------------
/features/src/main/res/layout/fragment_blog.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
15 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/model/CachedBlogPost.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import java.text.SimpleDateFormat
7 | import java.util.*
8 |
9 | @Entity(tableName = "blog_post")
10 | data class CachedBlogPost(
11 |
12 | @PrimaryKey(autoGenerate = false)
13 | @ColumnInfo(name = "pk")
14 | var pk: Int,
15 |
16 | @ColumnInfo(name = "title")
17 | var title: String? = null,
18 |
19 | @ColumnInfo(name = "slug")
20 | var slug: String? = null,
21 |
22 | @ColumnInfo(name = "body")
23 | var body: String? = null,
24 |
25 | @ColumnInfo(name = "image")
26 | var image: String? = null,
27 |
28 | @ColumnInfo(name = "date_updated")
29 | var date_updated: Long? = null,
30 |
31 | @ColumnInfo(name = "username")
32 | var username: String? = null
33 | ) {
34 |
35 | fun getDateAsString(): String {
36 | val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
37 | try {
38 | return sdf.format(Date(date_updated!!))
39 | } catch (e: Exception) {
40 | throw Exception(e)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/koin/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.koin
2 |
3 | import com.mi.mvi.domain.usecase.account.ChangePasswordUseCase
4 | import com.mi.mvi.domain.usecase.account.GetAccountUseCase
5 | import com.mi.mvi.domain.usecase.account.UpdateAccountUseCase
6 | import com.mi.mvi.domain.usecase.auth.CheckTokenUseCase
7 | import com.mi.mvi.domain.usecase.auth.LoginUseCase
8 | import com.mi.mvi.domain.usecase.auth.RegisterUseCase
9 | import com.mi.mvi.domain.usecase.blogs.*
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import org.koin.dsl.module
12 |
13 | @ExperimentalCoroutinesApi
14 | val useCaseModule = module {
15 | factory { LoginUseCase(get()) }
16 | factory { RegisterUseCase(get()) }
17 | factory { CheckTokenUseCase(get()) }
18 | factory { GetAccountUseCase(get()) }
19 | factory { UpdateAccountUseCase(get()) }
20 | factory { ChangePasswordUseCase(get()) }
21 | factory { SearchBlogUseCase(get()) }
22 | factory { IsAuthorBlogPostUseCase(get()) }
23 | factory { DeleteBlogPostUseCase(get()) }
24 | factory { UpdateBlogPostUseCase(get()) }
25 | factory { CreateBlogUseCase(get()) }
26 | factory { FiltrationUseCase(get()) }
27 | }
28 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/mapper/BlogPostListEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.mapper
2 |
3 | import com.mi.mvi.data.entity.BlogPostEntity
4 | import com.mi.mvi.data.entity.BlogPostListEntity
5 | import com.mi.mvi.remote.model.RemoteBlogPostList
6 |
7 | /**
8 | * Map a [RemoteBlogPostList] to and from a [BlogPostListEntity] instance when data is moving between
9 | * this later and the Data layer
10 | */
11 | open class BlogPostListEntityMapper : EntityMapper {
12 |
13 | /**
14 | * Map an instance of a [RemoteBlogPostList] to a [BlogPostListEntity] model
15 | */
16 | override fun mapFromRemote(type: RemoteBlogPostList): BlogPostListEntity {
17 | val itemsEntity = type.results.map { itemRemote ->
18 | BlogPostEntity(
19 | itemRemote.pk,
20 | itemRemote.title,
21 | itemRemote.slug,
22 | itemRemote.body,
23 | itemRemote.image,
24 | itemRemote.date_updated,
25 | itemRemote.username
26 | )
27 | }.toMutableList()
28 |
29 | return BlogPostListEntity(itemsEntity, type.detail)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/source/AuthRemoteDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.source
2 |
3 | import com.mi.mvi.data.datasource.remote.AuthRemoteDataSource
4 | import com.mi.mvi.data.entity.UserEntity
5 | import com.mi.mvi.remote.mapper.UserEntityMapper
6 | import com.mi.mvi.remote.service.AuthAPIService
7 |
8 | class AuthRemoteDataSourceImpl(
9 | private val authAPIService: AuthAPIService,
10 | private val userEntityMapper: UserEntityMapper
11 | ) : AuthRemoteDataSource {
12 |
13 | override suspend fun login(email: String, password: String): UserEntity {
14 | return userEntityMapper.mapFromRemote(
15 | authAPIService.login(
16 | email,
17 | password
18 | )
19 | )
20 | }
21 |
22 | override suspend fun register(
23 | email: String,
24 | username: String,
25 | password: String,
26 | password2: String
27 | ): UserEntity {
28 | return userEntityMapper.mapFromRemote(
29 | authAPIService.register(
30 | email,
31 | username,
32 | password,
33 | password2
34 | )
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/features/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
20 |
21 |
23 |
24 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/repository/BlogRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.repository
2 |
3 | import com.mi.mvi.domain.datastate.DataState
4 | import com.mi.mvi.domain.model.BlogPost
5 | import com.mi.mvi.domain.model.Token
6 | import com.mi.mvi.domain.viewstate.BlogViewState
7 | import kotlinx.coroutines.flow.Flow
8 | import okhttp3.MultipartBody
9 | import okhttp3.RequestBody
10 |
11 | interface BlogRepository : BaseRepository {
12 |
13 | fun searchBlogPosts(
14 | token: Token,
15 | query: String,
16 | filterAndOrder: String,
17 | page: Int
18 | ): Flow>
19 |
20 | fun isAuthorOfBlogPosts(
21 | token: Token,
22 | slug: String
23 | ): Flow>
24 |
25 | fun deleteBlogPost(
26 | token: Token,
27 | blogPost: BlogPost
28 | ): Flow>
29 |
30 | fun updateBlogPost(
31 | token: Token,
32 | slug: String,
33 | title: RequestBody,
34 | body: RequestBody,
35 | image: MultipartBody.Part?
36 | ): Flow>
37 |
38 | fun saveFilterOptions(filter: String, order: String)
39 |
40 | fun getFilter(): String?
41 |
42 | fun getOrder(): String?
43 | }
44 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/auth/RegisterFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.auth
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import com.mi.mvi.R
6 | import com.mi.mvi.base.BaseFragment
7 | import com.mi.mvi.events.AuthEventState
8 | import kotlinx.android.synthetic.main.fragment_register.*
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.FlowPreview
11 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel
12 |
13 | @FlowPreview
14 | @ExperimentalCoroutinesApi
15 | class RegisterFragment : BaseFragment(R.layout.fragment_register) {
16 |
17 | private val viewModel: AuthViewModel by sharedViewModel()
18 |
19 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
20 | super.onViewCreated(view, savedInstanceState)
21 |
22 | btnRegister.setOnClickListener { register() }
23 | }
24 |
25 | private fun register() {
26 | viewModel.setEventState(
27 | AuthEventState.RegisterEvent(
28 | input_email.text.toString(),
29 | input_username.text.toString(),
30 | input_password.text.toString(),
31 | input_password_confirm.text.toString()
32 | )
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/Adapters.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog
2 |
3 | import com.mi.mvi.R
4 | import com.mi.mvi.domain.model.BlogPostView
5 | import com.mi.mvi.features.main.blog.viewmodel.BlogListItem
6 | import me.ibrahimyilmaz.kiel.adapterOf
7 |
8 | internal fun createBlogListAdapter(onItemSelected: (BlogPostView) -> Unit) =
9 | adapterOf {
10 | diff(
11 | areItemsTheSame = { old, new ->
12 | when {
13 | old is BlogListItem.Item && new is BlogListItem.Item
14 | -> old.blogPostView.pk == new.blogPostView.pk
15 | else -> old === new
16 | }
17 | },
18 | areContentsTheSame = { old, new -> old == new }
19 | )
20 | register(
21 | layoutResource = R.layout.layout_no_more_results,
22 | viewHolder = ::GenericViewHolder
23 | )
24 | register(
25 | layoutResource = R.layout.layout_blog_list_item,
26 | viewHolder = ::BlogViewHolder,
27 | onBindViewHolder = { viewHolder, _, item ->
28 | viewHolder.itemView.setOnClickListener {
29 | onItemSelected(item.blogPostView)
30 | }
31 | }
32 | )
33 | }
--------------------------------------------------------------------------------
/features/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIB)
3 | id(GradlePluginId.BASE_GRADLE_PLUGIN)
4 | id(GradlePluginId.SAFE_ARGS)
5 | `kotlin-kapt`
6 | }
7 |
8 | dependencies {
9 |
10 | implementation(LibraryDependency.CONSTRAINT)
11 | implementation(LibraryDependency.APPCOMPAT)
12 | implementation(LibraryDependency.MATERIAL)
13 | implementation(LibraryDependency.RECYCYLER_VIEW)
14 | implementation(LibraryDependency.CARD_VIEW)
15 | implementation(LibraryDependency.SWIPE_TO_REFERESH)
16 | implementation(LibraryDependency.NAVIGATION_FRAGMENT)
17 | implementation(LibraryDependency.NAVIGATION_UI)
18 | implementation(LibraryDependency.NAVIGATION_RUNTIME)
19 | implementation(LibraryDependency.MATERIAL_DIALOG)
20 | implementation(LibraryDependency.CORE_KTX)
21 | implementation(LibraryDependency.CROP)
22 | implementation(LibraryDependency.GLIDE)
23 | kapt(LibraryDependency.GLIDE_COMPILAR)
24 | implementation(LibraryDependency.LIVE_DATA_RUNTIME)
25 | kapt(LibraryDependency.LIVE_DATA_COMPILER)
26 | implementation(LibraryDependency.LIVE_DATA_KTX)
27 | implementation(LibraryDependency.OKHTTP)
28 | implementation(LibraryDependency.KIEL)
29 |
30 | implementation(project(ModulesDependency.DOMAIN))
31 |
32 | addTestDependencies()
33 | }
34 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/mapper/BlogPostEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.mapper
2 |
3 | import com.mi.mvi.cache.model.CachedBlogPost
4 | import com.mi.mvi.data.entity.BlogPostEntity
5 |
6 | /**
7 | * Map a [CachedBlogPost] instance to and from a [BlogPostEntity] instance when data is moving between
8 | * this later and the Data layer
9 | */
10 | class BlogPostEntityMapper : EntityMapper {
11 |
12 | /**
13 | * Map a [CachedBlogPost] instance to a [BlogPostEntity] instance
14 | */
15 | override fun mapFromCached(type: CachedBlogPost): BlogPostEntity {
16 | return BlogPostEntity(
17 | type.pk,
18 | type.title,
19 | type.slug,
20 | type.body,
21 | type.image,
22 | type.getDateAsString(),
23 | type.username
24 | )
25 | }
26 |
27 | /**
28 | * Map a [BlogPostEntity] instance to a [CachedBlogPost] instance
29 | */
30 | override fun mapToCached(type: BlogPostEntity): CachedBlogPost {
31 | return CachedBlogPost(
32 | type.pk,
33 | type.title,
34 | type.slug,
35 | type.body,
36 | type.image,
37 | type.getDateAsLong(),
38 | type.username
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
17 |
18 |
28 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/red_button_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
9 |
12 |
13 |
14 |
15 | -
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 | -
25 |
26 |
27 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/model/RemoteBlogPost.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 | import java.text.SimpleDateFormat
6 | import java.util.*
7 |
8 | data class RemoteBlogPost(
9 | @SerializedName("response") @Expose var response: String? = null,
10 | @SerializedName("pk") @Expose var pk: Int,
11 | @SerializedName("title") @Expose var title: String? = null,
12 | @SerializedName("slug") @Expose var slug: String? = null,
13 | @SerializedName("body") @Expose var body: String? = null,
14 | @SerializedName("image") @Expose var image: String? = null,
15 | @SerializedName("date_updated") @Expose var date_updated: String? = null,
16 | @SerializedName("username") @Expose var username: String? = null
17 | ) {
18 |
19 | // dates from server look like this: "2019-07-23T03:28:01.406944Z"
20 | fun getDateAsLong(): Long {
21 |
22 | date_updated?.let {
23 | val stringDate =
24 | it.removeRange(it.indexOf("T") until it.length)
25 | val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
26 | try {
27 | return sdf.parse(stringDate).time
28 | } catch (e: Exception) {
29 | throw Exception(e)
30 | }
31 | } ?: return 0L
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable/main_button_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
9 |
12 |
13 |
14 |
15 | -
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 | -
25 |
26 |
27 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | # -------Gradle--------
10 | org.gradle.jvmargs=-Xmx4g
11 | org.gradle.daemon=true
12 | org.gradle.parallel=true
13 | org.gradle.caching=true
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 | # AndroidX package structure to make it clearer which packages are bundled with the
19 | # Android operating system, and which are packaged with your app"s APK
20 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
21 | # -------Android-------
22 | android.useAndroidX=true
23 | # Automatically convert third-party libraries to use AndroidX
24 | android.enableJetifier=true
25 | # Kotlin code style for this project: "official" or "obsolete":
26 | # -------Kotlin--------
27 | kotlin.code.style=official
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/koin/MainModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.koin
2 |
3 | import com.mi.mvi.common.SessionManager
4 | import com.mi.mvi.features.main.account.AccountViewModel
5 | import com.mi.mvi.features.main.blog.viewmodel.BlogViewModel
6 | import com.mi.mvi.features.main.create_blog.CreateBlogViewModel
7 | import com.mi.mvi.mapper.BlogPostMapper
8 | import com.mi.mvi.mapper.TokenMapper
9 | import com.mi.mvi.mapper.UserMapper
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.FlowPreview
12 | import org.koin.android.ext.koin.androidContext
13 | import org.koin.androidx.viewmodel.dsl.viewModel
14 | import org.koin.dsl.module
15 |
16 | @FlowPreview
17 | @ExperimentalCoroutinesApi
18 | val mainModule = module {
19 | single { SessionManager(get(), androidContext()) }
20 |
21 | factory { TokenMapper() }
22 | factory { UserMapper() }
23 | factory { BlogPostMapper() }
24 | viewModel { AccountViewModel(get(), get(), get(), get(), get(), get()) }
25 |
26 | viewModel {
27 | BlogViewModel(
28 | get(),
29 | get(),
30 | get(),
31 | get(),
32 | get(),
33 | get(),
34 | get(),
35 | get()
36 | )
37 | }
38 |
39 | viewModel {
40 | CreateBlogViewModel(
41 | get(),
42 | get(),
43 | get()
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Kotlin MVI Clean Architecture APP
2 |
3 | [](https://kotlinlang.org/) [](https://www.youtube.com/watch?v=tIPxSWx5qpk) [](https://developer.android.com/kotlin/coroutines) [](https://kotlinlang.org/docs/tutorials/android-plugin.html)
4 |
5 |
6 | - Modularization
7 | - Gradle Dependency management
8 | - Gradle written in Kotlin DSL
9 | - Custom Plugin (dependencies with no duplication)
10 | - Navigation Components
11 | - [Coroutines](https://developer.android.com/kotlin/coroutines) and flows
12 | - [Room Persistence Library](https://developer.android.com/training/data-storage/room "Room Persistence Library")
13 | - Dependency Injection/Service Locator with [Koin](https://github.com/InsertKoinIO/koin "Koin") Library.
14 | - Model View Intent Architecture - MVI.
15 | - Repository pattern (NetworkBoundResource)
16 | - Clean Architecture approach.
17 | - Static Code Analytics [Ktlint](https://github.com/jlleitschuh/ktlint-gradle "Ktlint") This plugin creates convenient tasks in your Gradle project that run ktlint checks or do code auto format.
18 |
19 |
20 |
21 |
22 |
23 | Discussions
24 | -
25 | Refer to the issues section: https://github.com/MoIbrahim15/Android-Kotlin-MVI-CleanArchitecture/issues
26 |
27 | Contacts
28 | -
29 | [LinkedIn](https://www.linkedin.com/in/mohamedibrahim15/)
30 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/koin/DataModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.koin
2 |
3 | import com.mi.mvi.data.mapper.BlogPostMapper
4 | import com.mi.mvi.data.mapper.TokenMapper
5 | import com.mi.mvi.data.mapper.UserMapper
6 | import com.mi.mvi.data.repository.AccountRepositoryImpl
7 | import com.mi.mvi.data.repository.AuthRepositoryImpl
8 | import com.mi.mvi.data.repository.BlogRepositoryImpl
9 | import com.mi.mvi.data.repository.CreateBlogRepositoryImpl
10 | import com.mi.mvi.domain.repository.AccountRepository
11 | import com.mi.mvi.domain.repository.AuthRepository
12 | import com.mi.mvi.domain.repository.BlogRepository
13 | import com.mi.mvi.domain.repository.CreateBlogRepository
14 | import kotlinx.coroutines.ExperimentalCoroutinesApi
15 | import org.koin.dsl.module
16 |
17 | @ExperimentalCoroutinesApi
18 | val dataModule = module {
19 |
20 | factory { TokenMapper() }
21 | factory { BlogPostMapper() }
22 | factory { UserMapper() }
23 |
24 | factory {
25 | AuthRepositoryImpl(
26 | get(),
27 | get(),
28 | get(),
29 | get()
30 | )
31 | }
32 | factory {
33 | AccountRepositoryImpl(
34 | get(),
35 | get(),
36 | get()
37 | )
38 | }
39 | factory {
40 | BlogRepositoryImpl(
41 | get(),
42 | get(),
43 | get()
44 | )
45 | }
46 | factory {
47 | CreateBlogRepositoryImpl(
48 | get(),
49 | get()
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/auth/LoginFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.auth
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.navigation.fragment.findNavController
6 | import com.mi.mvi.R
7 | import com.mi.mvi.base.BaseFragment
8 | import com.mi.mvi.events.AuthEventState
9 | import kotlinx.android.synthetic.main.fragment_login.*
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.FlowPreview
12 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel
13 |
14 | @FlowPreview
15 | @ExperimentalCoroutinesApi
16 | class LoginFragment : BaseFragment(R.layout.fragment_login) {
17 |
18 | private val viewModel: AuthViewModel by sharedViewModel()
19 |
20 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
21 | super.onViewCreated(view, savedInstanceState)
22 |
23 | btnForget.setOnClickListener {
24 | navForgetPassword()
25 | }
26 | btnRegister.setOnClickListener { navRegistration() }
27 |
28 | btnLogin.setOnClickListener { login() }
29 | }
30 |
31 | private fun login() {
32 | viewModel.setEventState(
33 | AuthEventState.LoginEvent(
34 | input_email.text.toString(),
35 | input_password.text.toString()
36 | )
37 | )
38 | }
39 |
40 | private fun navRegistration() {
41 | findNavController().navigate(R.id.action_loginFragment_to_registerFragment)
42 | }
43 |
44 | private fun navForgetPassword() {
45 | findNavController().navigate(R.id.action_loginFragment_to_forgetPasswordFragment)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/common/SessionManager.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.common
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.mi.mvi.domain.repository.AuthRepository
7 | import com.mi.mvi.model.TokenView
8 | import kotlinx.coroutines.CancellationException
9 | import kotlinx.coroutines.Dispatchers.IO
10 | import kotlinx.coroutines.Dispatchers.Main
11 | import kotlinx.coroutines.GlobalScope
12 | import kotlinx.coroutines.launch
13 |
14 | class SessionManager(
15 | private val authRepository: AuthRepository,
16 | val context: Context
17 | ) {
18 | private val _cachedToken = MutableLiveData()
19 |
20 | val cachedTokenViewEntity: LiveData
21 | get() = _cachedToken
22 |
23 | fun login(newValue: TokenView) {
24 | setValue(newValue)
25 | }
26 |
27 | fun logout() {
28 | GlobalScope.launch(IO) {
29 | var errorMessage: String? = null
30 |
31 | try {
32 | _cachedToken.value?.account_pk?.let { pk ->
33 | authRepository.nullifyToken(pk)
34 | }
35 | } catch (e: CancellationException) {
36 | errorMessage = e.message
37 | } catch (e: Exception) {
38 | errorMessage = errorMessage + "\n" + e.message
39 | } finally {
40 | }
41 | setValue(null)
42 | }
43 | }
44 |
45 | fun setValue(newValue: TokenView?) {
46 | GlobalScope.launch(Main) {
47 | if (_cachedToken.value != newValue) {
48 | _cachedToken.value = newValue!!
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/service/BlogAPIService.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.service
2 |
3 | import com.mi.mvi.remote.model.BaseRemote
4 | import com.mi.mvi.remote.model.RemoteBlogPost
5 | import com.mi.mvi.remote.model.RemoteBlogPostList
6 | import okhttp3.MultipartBody
7 | import okhttp3.RequestBody
8 | import retrofit2.http.*
9 |
10 | interface BlogAPIService {
11 |
12 | @GET("blog/list")
13 | suspend fun searchListBlogPosts(
14 | @Header("Authorization") authorization: String,
15 | @Query("search") query: String,
16 | @Query("ordering") ordering: String,
17 | @Query("page") page: Int
18 | ): RemoteBlogPostList
19 |
20 | @GET("blog/{slug}/is_author")
21 | suspend fun isAuthorOfBlogPost(
22 | @Header("Authorization") authorization: String,
23 | @Path("slug") slug: String
24 | ): BaseRemote
25 |
26 | @DELETE("blog/{slug}/delete")
27 | suspend fun deleteBlogPost(
28 | @Header("Authorization") authorization: String,
29 | @Path("slug") slug: String?
30 | ): BaseRemote
31 |
32 | @Multipart
33 | @PUT("blog/{slug}/update")
34 | suspend fun updateBlog(
35 | @Header("Authorization") authorization: String,
36 | @Path("slug") slug: String,
37 | @Part("title") title: RequestBody,
38 | @Part("body") body: RequestBody,
39 | @Part image: MultipartBody.Part?
40 | ): RemoteBlogPost
41 |
42 | @Multipart
43 | @POST("blog/create")
44 | suspend fun createBlog(
45 | @Header("Authorization") authorization: String,
46 | @Part("title") title: RequestBody,
47 | @Part("body") body: RequestBody,
48 | @Part image: MultipartBody.Part?
49 | ): RemoteBlogPost
50 | }
51 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/viewmodel/Pagination.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog.viewmodel
2 |
3 | import com.mi.mvi.domain.viewstate.BlogViewState
4 | import com.mi.mvi.events.BlogEventState.BlogSearchEvent
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.FlowPreview
7 |
8 | @FlowPreview
9 | @ExperimentalCoroutinesApi
10 | fun BlogViewModel.resetPage() {
11 | val update = getCurrentViewStateOrNew()
12 | update.blogFields.page = 1
13 | setViewState(update)
14 | }
15 |
16 | @FlowPreview
17 | @ExperimentalCoroutinesApi
18 | fun BlogViewModel.refreshFromCache() {
19 | setQueryExhausted(false)
20 | setEventState(BlogSearchEvent)
21 | }
22 |
23 | @FlowPreview
24 | @ExperimentalCoroutinesApi
25 | fun BlogViewModel.loadFirstPage() {
26 | setQueryExhausted(false)
27 | resetPage()
28 | setEventState(BlogSearchEvent)
29 | }
30 |
31 | @FlowPreview
32 | @ExperimentalCoroutinesApi
33 | private fun BlogViewModel.incrementPageNumber() {
34 | val update = getCurrentViewStateOrNew()
35 | val page = update.copy().blogFields.page ?: 1
36 | update.blogFields.page = page.plus(1)
37 | setViewState(update)
38 | }
39 |
40 | @FlowPreview
41 | @ExperimentalCoroutinesApi
42 | fun BlogViewModel.nextPage() {
43 | if (!getIsQueryExhausted()) {
44 | incrementPageNumber()
45 | setEventState(BlogSearchEvent)
46 | }
47 | }
48 |
49 | @FlowPreview
50 | @ExperimentalCoroutinesApi
51 | fun BlogViewModel.handleIncomingBlogListData(viewState: BlogViewState) {
52 | viewState.blogFields.let { blogFields ->
53 | blogFields.blogList?.let {
54 | setBlogListData(it.map { blogPostMapper.mapToView(it) }.toMutableList())
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/features/src/main/res/navigation/nav_auth.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
20 |
27 |
28 |
29 |
34 |
35 |
39 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/source/AccountRemoteDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.source
2 |
3 | import com.mi.mvi.data.datasource.remote.AccountRemoteDataSource
4 | import com.mi.mvi.data.entity.BaseEntity
5 | import com.mi.mvi.data.entity.UserEntity
6 | import com.mi.mvi.remote.mapper.BaseEntityMapper
7 | import com.mi.mvi.remote.mapper.UserEntityMapper
8 | import com.mi.mvi.remote.service.AccountAPIService
9 |
10 | class AccountRemoteDataSourceImpl(
11 | private val accountAPIService: AccountAPIService,
12 | private val userEntityMapper: UserEntityMapper,
13 | private val baseEntityMapper: BaseEntityMapper
14 | ) : AccountRemoteDataSource {
15 |
16 | override suspend fun getAccountProperties(authorization: String): UserEntity {
17 | return userEntityMapper.mapFromRemote(accountAPIService.getAccountProperties(authorization))
18 | }
19 |
20 | override suspend fun updateAccountProperties(
21 | authorization: String,
22 | email: String?,
23 | username: String?
24 | ): BaseEntity {
25 | return baseEntityMapper.mapFromRemote(
26 | accountAPIService.updateAccountProperties(
27 | authorization,
28 | email,
29 | username
30 | )
31 | )
32 | }
33 |
34 | override suspend fun changePassword(
35 | authorization: String,
36 | currentPassword: String,
37 | newPassword: String,
38 | confirmNewPassword: String
39 | ): BaseEntity {
40 | return baseEntityMapper.mapFromRemote(
41 | accountAPIService.changePassword(
42 | authorization,
43 | currentPassword,
44 | newPassword,
45 | confirmNewPassword
46 | )
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/activity_auth.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
18 |
19 |
26 |
27 |
28 |
29 |
40 |
41 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/cache/BlogCacheQueries.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.cache
2 |
3 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource.Companion.ORDER_BY_ASC_DATE_UPDATED
4 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource.Companion.ORDER_BY_ASC_USERNAME
5 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource.Companion.ORDER_BY_DESC_DATE_UPDATED
6 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource.Companion.ORDER_BY_DESC_USERNAME
7 | import com.mi.mvi.data.entity.BlogPostEntity
8 |
9 | suspend fun BlogCacheDataSource.returnOrderedBlogQuery(
10 | filterAndOrder: String,
11 | query: String,
12 | page: Int
13 | ): MutableList {
14 |
15 | when {
16 |
17 | filterAndOrder.contains(ORDER_BY_DESC_DATE_UPDATED) -> {
18 | return searchBlogPostsOrderByDateDESC(
19 | query = query,
20 | page = page
21 | )
22 | }
23 |
24 | filterAndOrder.contains(ORDER_BY_ASC_DATE_UPDATED) -> {
25 | return searchBlogPostsOrderByDateASC(
26 | query = query,
27 | page = page
28 | )
29 | }
30 |
31 | filterAndOrder.contains(ORDER_BY_DESC_USERNAME) -> {
32 | return searchBlogPostsOrderByAuthorDESC(
33 | query = query,
34 | page = page
35 | )
36 | }
37 |
38 | filterAndOrder.contains(ORDER_BY_ASC_USERNAME) -> {
39 | return searchBlogPostsOrderByAuthorASC(
40 | query = query,
41 | page = page
42 | )
43 | }
44 | else ->
45 | return searchBlogPostsOrderByDateASC(
46 | query = query,
47 | page = page
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/koin/DataBaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.koin
2 |
3 | import androidx.room.Room
4 | import com.mi.mvi.cache.db.AppDatabase
5 | import com.mi.mvi.cache.mapper.BlogPostEntityMapper
6 | import com.mi.mvi.cache.mapper.TokenEntityMapper
7 | import com.mi.mvi.cache.mapper.UserEntityMapper
8 | import com.mi.mvi.cache.source.AccountCacheDataSourceImpl
9 | import com.mi.mvi.cache.source.BlogCacheDataSourceImpl
10 | import com.mi.mvi.cache.source.TokenCacheDataSourceImpl
11 | import com.mi.mvi.data.datasource.cache.AccountCacheDataSource
12 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource
13 | import com.mi.mvi.data.datasource.cache.TokenCacheDataSource
14 | import org.koin.android.ext.koin.androidContext
15 | import org.koin.dsl.module
16 |
17 | val databaseModule = module {
18 | single {
19 | Room.databaseBuilder(
20 | androidContext(),
21 | AppDatabase::class.java,
22 | AppDatabase.DATABASE_NAME
23 | ).build()
24 | }
25 | single { get().getAuthTokenDao() }
26 | single { get().getAccountDao() }
27 | single { get().getBlogPostDao() }
28 |
29 | factory { TokenEntityMapper() }
30 | factory { UserEntityMapper() }
31 | factory { BlogPostEntityMapper() }
32 |
33 | factory {
34 | TokenCacheDataSourceImpl(
35 | get(),
36 | get()
37 | )
38 | }
39 | factory {
40 | AccountCacheDataSourceImpl(
41 | get(),
42 | get(),
43 | get(),
44 | get()
45 | )
46 | }
47 |
48 | factory {
49 | BlogCacheDataSourceImpl(
50 | get(),
51 | get(),
52 | get(),
53 | get()
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/source/AccountCacheDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.source
2 |
3 | import android.content.SharedPreferences
4 | import com.mi.mvi.cache.db.AccountDao
5 | import com.mi.mvi.cache.mapper.UserEntityMapper
6 | import com.mi.mvi.data.datasource.cache.AccountCacheDataSource
7 | import com.mi.mvi.data.entity.UserEntity
8 |
9 | const val PREVIOUS_AUTH_USER: String = "com.mi.mvi.PREVIOUS_AUTH_USER"
10 |
11 | class AccountCacheDataSourceImpl(
12 | private val accountDao: AccountDao,
13 | private val userEntityMapper: UserEntityMapper,
14 | private val sharedPreferences: SharedPreferences,
15 | private val sharedPrefsEditor: SharedPreferences.Editor
16 | ) : AccountCacheDataSource {
17 |
18 | override suspend fun insertOrIgnore(userEntity: UserEntity): Long {
19 | return accountDao.insertOrIgnore(userEntityMapper.mapToCached(userEntity))
20 | }
21 |
22 | override suspend fun searchByPk(pk: Int): UserEntity? {
23 | accountDao.searchByPk(pk)?.let { cachedUser ->
24 | return userEntityMapper.mapFromCached(cachedUser)
25 | } ?: return null
26 | }
27 |
28 | override suspend fun searchByEmail(email: String): UserEntity? {
29 | accountDao.searchByEmail(email)?.let { cachedUser ->
30 | return userEntityMapper.mapFromCached(cachedUser)
31 | } ?: return null
32 | }
33 |
34 | override suspend fun updateAccountProperties(pk: Int, email: String?, username: String?) {
35 | return accountDao.updateAccountProperties(pk, email, username)
36 | }
37 |
38 | override fun getLoggedInEmail(): String? {
39 | return sharedPreferences.getString(PREVIOUS_AUTH_USER, null)
40 | }
41 |
42 | override fun saveLoggedInEmail(email: String?) {
43 | sharedPrefsEditor.putString(PREVIOUS_AUTH_USER, email)
44 | sharedPrefsEditor.apply()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/account/ChangePasswordFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.account
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.lifecycle.Observer
6 | import androidx.navigation.fragment.findNavController
7 | import com.mi.mvi.R
8 | import com.mi.mvi.domain.Constants.Companion.SUCCESS
9 | import com.mi.mvi.events.AccountEventState
10 | import kotlinx.android.synthetic.main.fragment_change_password.*
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.FlowPreview
13 |
14 | @FlowPreview
15 | @ExperimentalCoroutinesApi
16 | class ChangePasswordFragment : BaseAccountFragment(R.layout.fragment_change_password) {
17 |
18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
19 | super.onViewCreated(view, savedInstanceState)
20 | subscribeObservers()
21 |
22 | update_password_button.setOnClickListener {
23 | viewModel.setEventState(
24 | AccountEventState.ChangePasswordEvent(
25 | input_current_password.text.toString(),
26 | input_new_password.text.toString(),
27 | input_confirm_new_password.text.toString()
28 | )
29 | )
30 | }
31 | }
32 |
33 | private fun subscribeObservers() {
34 | viewModel.dataState.observe(viewLifecycleOwner, Observer { dataState ->
35 | dataState?.let {
36 | dataStateChangeListener?.onDataStateChangeListener(dataState)
37 | dataState.stateMessage?.let { stateMessage ->
38 | if (stateMessage.message == SUCCESS) {
39 | uiCommunicationListener?.hideSoftKeyboard()
40 | findNavController().popBackStack()
41 | }
42 | }
43 | }
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/common/ViewExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.common
2 |
3 | import android.app.Activity
4 | import android.widget.Toast
5 | import androidx.annotation.StringRes
6 | import com.afollestad.materialdialogs.MaterialDialog
7 | import com.mi.mvi.R
8 | import com.mi.mvi.domain.datastate.AreYouSureCallBack
9 |
10 | fun Activity.displayToast(msg: String?) {
11 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
12 | }
13 |
14 | fun Activity.displayToast(@StringRes msg: Int) {
15 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
16 | }
17 |
18 | // fixed leak memory
19 | var materialDialog: MaterialDialog? = null
20 |
21 | fun Activity.displaySuccessDialog(message: String) {
22 | materialDialog = MaterialDialog(this)
23 | .show {
24 | title(R.string.text_success)
25 | message(text = message)
26 | positiveButton(R.string.text_ok)
27 | }
28 | }
29 |
30 | fun Activity.displayErrorDialog(message: String) {
31 | materialDialog = MaterialDialog(this)
32 | .show {
33 | title(R.string.text_error)
34 | message(text = message)
35 | positiveButton(R.string.text_ok)
36 | }
37 | }
38 |
39 | fun Activity.displayInfoDialog(message: String?) {
40 | materialDialog = MaterialDialog(this)
41 | .show {
42 | title(R.string.are_you_sure)
43 | message(text = message)
44 | positiveButton(R.string.text_ok)
45 | }
46 | }
47 |
48 | fun Activity.areYouSureDialog(message: String?, callback: AreYouSureCallBack) {
49 | materialDialog = MaterialDialog(this)
50 | .show {
51 | title(R.string.text_info)
52 | message(text = message)
53 | positiveButton(R.string.text_yes) {
54 | callback.proceed()
55 | }
56 | negativeButton(R.string.text_cancel) {
57 | callback.cancel()
58 | materialDialog = null
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/auth/AuthViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.auth
2 |
3 | import com.mi.mvi.base.BaseViewModel
4 | import com.mi.mvi.domain.datastate.DataState
5 | import com.mi.mvi.domain.usecase.auth.CheckTokenUseCase
6 | import com.mi.mvi.domain.usecase.auth.LoginUseCase
7 | import com.mi.mvi.domain.usecase.auth.RegisterUseCase
8 | import com.mi.mvi.domain.viewstate.AuthViewState
9 | import com.mi.mvi.events.AuthEventState
10 | import com.mi.mvi.events.AuthEventState.*
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.FlowPreview
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.emitAll
15 | import kotlinx.coroutines.flow.flow
16 |
17 | @FlowPreview
18 | @ExperimentalCoroutinesApi
19 | class AuthViewModel(
20 | private val loginUseCase: LoginUseCase,
21 | private val registerUseCase: RegisterUseCase,
22 | private val checkTokenUseCase: CheckTokenUseCase
23 | ) : BaseViewModel() {
24 |
25 | override fun handleEventState(eventState: AuthEventState): Flow> =
26 | flow {
27 | when (eventState) {
28 | is LoginEvent -> {
29 | emitAll(loginUseCase.invoke(eventState.email, eventState.password))
30 | }
31 | is RegisterEvent -> {
32 | emitAll(
33 | registerUseCase.invoke(
34 | eventState.email,
35 | eventState.username,
36 | eventState.password,
37 | eventState.confirmPassword
38 | )
39 | )
40 | }
41 | is CheckTokenEvent -> {
42 | emitAll(checkTokenUseCase.invoke())
43 | }
44 | }
45 | }
46 |
47 | override fun initNewViewState(): AuthViewState {
48 | return AuthViewState()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/features/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/fragment_forget_password.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
23 |
24 |
30 |
31 |
38 |
39 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.base
2 |
3 | import androidx.lifecycle.*
4 | import com.mi.mvi.domain.datastate.DataState
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.FlowPreview
7 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
8 | import kotlinx.coroutines.flow.*
9 |
10 | @FlowPreview
11 | @ExperimentalCoroutinesApi
12 | abstract class BaseViewModel : ViewModel() {
13 | private val dataChannel: ConflatedBroadcastChannel> =
14 | ConflatedBroadcastChannel()
15 |
16 | // keep this protected so that only the ViewModel can modify the state
17 | protected val _viewState: MutableLiveData = MutableLiveData()
18 | // Create a publicly accessible LiveData object that can be observed
19 | val viewState: LiveData
20 | get() = _viewState
21 |
22 | // keep this protected so that only the ViewModel can modify the state
23 | protected val _dataState: MutableLiveData> = MutableLiveData()
24 | // Create a publicly accessible LiveData object that can be observed
25 | val dataState: LiveData> = _dataState
26 |
27 | init {
28 | dataChannel
29 | .asFlow()
30 | .onEach { dataState ->
31 | _dataState.value = dataState
32 | }
33 | .launchIn(viewModelScope)
34 | }
35 |
36 | fun setEventState(eventState: EventState) {
37 | dataChannel.let { channel ->
38 | handleEventState(eventState).onEach { data ->
39 | if (!channel.isClosedForSend) {
40 | channel.offer(data)
41 | }
42 | }
43 | }.launchIn(viewModelScope)
44 | }
45 |
46 | fun setViewState(viewState: ViewState) {
47 | _viewState.value = viewState
48 | }
49 |
50 | fun getCurrentViewStateOrNew(): ViewState {
51 | return viewState.value ?: initNewViewState()
52 | }
53 |
54 | abstract fun handleEventState(eventState: EventState): Flow>
55 |
56 | abstract fun initNewViewState(): ViewState
57 | }
58 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/datasource/cache/BlogCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.datasource.cache
2 |
3 | import com.mi.mvi.data.entity.BlogPostEntity
4 |
5 | interface BlogCacheDataSource {
6 |
7 | companion object {
8 | const val PAGINATION_PAGE_SIZE = 10
9 |
10 | const val BLOG_FILTER_USERNAME = "username"
11 | const val BLOG_FILTER_DATE_UPDATED = "date_updated"
12 | const val BLOG_ORDER_ASC: String = ""
13 | const val BLOG_ORDER_DESC: String = "-"
14 |
15 | const val ORDER_BY_ASC_DATE_UPDATED = BLOG_ORDER_ASC + BLOG_FILTER_DATE_UPDATED
16 | const val ORDER_BY_DESC_DATE_UPDATED = BLOG_ORDER_DESC + BLOG_FILTER_DATE_UPDATED
17 | const val ORDER_BY_ASC_USERNAME = BLOG_ORDER_ASC + BLOG_FILTER_USERNAME
18 | const val ORDER_BY_DESC_USERNAME = BLOG_ORDER_DESC + BLOG_FILTER_USERNAME
19 | }
20 | suspend fun insert(blogPostEntity: BlogPostEntity): Long
21 |
22 | suspend fun deleteBlogPost(blogPostEntity: BlogPostEntity)
23 |
24 | suspend fun updateBlogPost(pk: Int, title: String?, body: String?, image: String?)
25 |
26 | suspend fun getAllBlogPosts(
27 | query: String,
28 | page: Int,
29 | pageSize: Int = PAGINATION_PAGE_SIZE
30 | ): MutableList
31 |
32 | suspend fun searchBlogPostsOrderByDateDESC(
33 | query: String,
34 | page: Int,
35 | pageSize: Int = PAGINATION_PAGE_SIZE
36 | ): MutableList
37 |
38 | suspend fun searchBlogPostsOrderByDateASC(
39 | query: String,
40 | page: Int,
41 | pageSize: Int = PAGINATION_PAGE_SIZE
42 | ): MutableList
43 |
44 | suspend fun searchBlogPostsOrderByAuthorDESC(
45 | query: String,
46 | page: Int,
47 | pageSize: Int = PAGINATION_PAGE_SIZE
48 | ): MutableList
49 |
50 | suspend fun searchBlogPostsOrderByAuthorASC(
51 | query: String,
52 | page: Int,
53 | pageSize: Int = PAGINATION_PAGE_SIZE
54 | ): MutableList
55 |
56 | fun saveFilterOptions(filter: String, order: String)
57 |
58 | fun getFilter(): String?
59 |
60 | fun getOrder(): String?
61 | }
62 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/auth/AuthActivity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.auth
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.lifecycle.Observer
7 | import com.mi.mvi.R
8 | import com.mi.mvi.base.BaseActivity
9 | import com.mi.mvi.domain.datastate.DataState
10 | import com.mi.mvi.features.main.MainActivity
11 | import com.mi.mvi.mapper.TokenMapper
12 | import kotlinx.android.synthetic.main.activity_auth.*
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import kotlinx.coroutines.FlowPreview
15 | import org.koin.androidx.viewmodel.ext.android.viewModel
16 |
17 | @FlowPreview
18 | @ExperimentalCoroutinesApi
19 | class AuthActivity : BaseActivity(R.layout.activity_auth) {
20 |
21 | private val authViewModel: AuthViewModel by viewModel()
22 | private val tokenMapper: TokenMapper = TokenMapper()
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | subscribeObservers()
27 | }
28 |
29 | private fun subscribeObservers() {
30 | authViewModel.dataState.observe(this, Observer { dataState ->
31 | onDataStateChangeListener(dataState)
32 | when (dataState) {
33 | is DataState.SUCCESS -> {
34 | dataState.data?.let { viewState ->
35 | viewState.token?.let { authToken ->
36 | sessionManager.login(tokenMapper.mapToView(authToken))
37 | }
38 | }
39 | }
40 | }
41 | })
42 |
43 | sessionManager.cachedTokenViewEntity.observe(
44 | this,
45 | Observer { authToken ->
46 | authToken?.let {
47 | if (it.account_pk != -1 && it.token != null) {
48 | navMainActivity()
49 | }
50 | }
51 | })
52 | }
53 |
54 | private fun navMainActivity() {
55 | startActivity(Intent(this, MainActivity::class.java))
56 | finish()
57 | }
58 |
59 | override fun displayLoading(isLoading: Boolean) {
60 | progress_bar.visibility = if (isLoading) {
61 | View.VISIBLE
62 | } else {
63 | View.GONE
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/viewstate/AuthViewState.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain.viewstate
2 |
3 | import com.mi.mvi.domain.Constants.Companion.ERROR_ALL_FIELDS_ARE_REQUIRED
4 | import com.mi.mvi.domain.Constants.Companion.ERROR_PASSWORD_DOESNOT_MATCH
5 | import com.mi.mvi.domain.Constants.Companion.SUCCESS
6 | import com.mi.mvi.domain.model.Token
7 |
8 | data class AuthViewState(
9 | var registrationFields: RegistrationFields? = null,
10 | var loginFields: LoginFields? = null,
11 | var token: Token? = null
12 | )
13 |
14 | data class RegistrationFields(
15 | var email: String? = null,
16 | var username: String? = null,
17 | var password: String? = null,
18 | var confirmPassword: String? = null
19 | ) {
20 |
21 | class RegistrationError {
22 | companion object {
23 |
24 | fun mustFillAllFields(): String {
25 | return ERROR_ALL_FIELDS_ARE_REQUIRED
26 | }
27 |
28 | fun passwordsDoNotMatch(): String {
29 | return ERROR_PASSWORD_DOESNOT_MATCH
30 | }
31 |
32 | fun none(): String {
33 | return SUCCESS
34 | }
35 | }
36 | }
37 |
38 | fun isValidForRegistration(): String {
39 | return if (email.isNullOrEmpty() ||
40 | username.isNullOrEmpty() ||
41 | password.isNullOrEmpty() ||
42 | confirmPassword.isNullOrEmpty()
43 | ) {
44 | RegistrationError.mustFillAllFields()
45 | } else if (!password.equals(confirmPassword)) {
46 | RegistrationError.passwordsDoNotMatch()
47 | } else {
48 | RegistrationError.none()
49 | }
50 | }
51 | }
52 |
53 | data class LoginFields(
54 | var email: String? = null,
55 | var password: String? = null
56 | ) {
57 |
58 | class LoginError {
59 | companion object {
60 | fun mustFillAllFields(): String {
61 | return ERROR_ALL_FIELDS_ARE_REQUIRED
62 | }
63 |
64 | fun none(): String {
65 | return SUCCESS
66 | }
67 | }
68 | }
69 |
70 | fun isValidForLogin(): String {
71 | return if (email.isNullOrEmpty() || password.isNullOrEmpty()) {
72 | LoginError.mustFillAllFields()
73 | } else LoginError.none()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/account/UpdateAccountFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.account
2 |
3 | import android.os.Bundle
4 | import android.view.Menu
5 | import android.view.MenuInflater
6 | import android.view.MenuItem
7 | import android.view.View
8 | import androidx.lifecycle.Observer
9 | import com.mi.mvi.R
10 | import com.mi.mvi.events.AccountEventState
11 | import com.mi.mvi.model.UserView
12 | import kotlinx.android.synthetic.main.fragment_update_account.*
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import kotlinx.coroutines.FlowPreview
15 |
16 | @FlowPreview
17 | @ExperimentalCoroutinesApi
18 | class UpdateAccountFragment : BaseAccountFragment(R.layout.fragment_update_account) {
19 |
20 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
21 | super.onViewCreated(view, savedInstanceState)
22 | setHasOptionsMenu(true)
23 | subscribeObservers()
24 | }
25 |
26 | private fun subscribeObservers() {
27 | viewModel.dataState.observe(viewLifecycleOwner, Observer { dataState ->
28 | dataState?.let {
29 | dataStateChangeListener?.onDataStateChangeListener(dataState)
30 | }
31 | })
32 |
33 | viewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
34 | if (viewState != null) {
35 | viewState.user?.let { accountProperties ->
36 | setAccountProperties(userEntity = userMapper.mapToView(accountProperties))
37 | }
38 | }
39 | })
40 | }
41 |
42 | private fun setAccountProperties(userEntity: UserView) {
43 | input_email.setText(userEntity.email)
44 | input_username.setText(userEntity.username)
45 | input_email.setText(userEntity.email)
46 | }
47 |
48 | private fun saveChanges() {
49 | viewModel.setEventState(
50 | AccountEventState.UpdateAccountEvent(
51 | input_email.text.toString(),
52 | input_username.text.toString()
53 | )
54 | )
55 | uiCommunicationListener?.hideSoftKeyboard()
56 | }
57 |
58 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
59 | super.onCreateOptionsMenu(menu, inflater)
60 | inflater.inflate(R.menu.update_menu, menu)
61 | }
62 |
63 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
64 | when (item.itemId) {
65 | R.id.save -> saveChanges()
66 | }
67 | return super.onOptionsItemSelected(item)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/blog/viewmodel/Getters.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.blog.viewmodel
2 |
3 | import android.net.Uri
4 | import androidx.core.net.toUri
5 | import com.mi.mvi.domain.model.BlogPost
6 | import com.mi.mvi.features.main.blog.BLOG_FILTER_DATE_UPDATED
7 | import com.mi.mvi.features.main.blog.BLOG_ORDER_DESC
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.FlowPreview
10 |
11 | @FlowPreview
12 | @ExperimentalCoroutinesApi
13 | fun BlogViewModel.getIsQueryExhausted(): Boolean {
14 | return getCurrentViewStateOrNew().blogFields.isQueryExhausted
15 | ?: false
16 | }
17 |
18 | @FlowPreview
19 | @ExperimentalCoroutinesApi
20 | fun BlogViewModel.getFilter(): String {
21 | return getCurrentViewStateOrNew().blogFields.filter
22 | ?: BLOG_FILTER_DATE_UPDATED
23 | }
24 |
25 | @FlowPreview
26 | @ExperimentalCoroutinesApi
27 | fun BlogViewModel.getOrder(): String {
28 | return getCurrentViewStateOrNew().blogFields.order
29 | ?: BLOG_ORDER_DESC
30 | }
31 |
32 | @FlowPreview
33 | @ExperimentalCoroutinesApi
34 | fun BlogViewModel.getSearchQuery(): String {
35 | return getCurrentViewStateOrNew().blogFields.searchQuery
36 | ?: return ""
37 | }
38 |
39 | @FlowPreview
40 | @ExperimentalCoroutinesApi
41 | fun BlogViewModel.getPage(): Int {
42 | return getCurrentViewStateOrNew().blogFields.page
43 | ?: return 1
44 | }
45 |
46 | @FlowPreview
47 | @ExperimentalCoroutinesApi
48 | fun BlogViewModel.getSlug(): String {
49 | getCurrentViewStateOrNew().let {
50 | it.viewBlogFields.blogPostEntity?.let {
51 | return it.slug!!
52 | }
53 | }
54 | return ""
55 | }
56 |
57 | @FlowPreview
58 | @ExperimentalCoroutinesApi
59 | fun BlogViewModel.isAuthorOfBlogPost(): Boolean {
60 | return getCurrentViewStateOrNew().viewBlogFields.isAuthor
61 | ?: false
62 | }
63 |
64 | @FlowPreview
65 | @ExperimentalCoroutinesApi
66 | fun BlogViewModel.getBlogPost(): BlogPost {
67 | getCurrentViewStateOrNew().let {
68 | return it.viewBlogFields.blogPostEntity?.let {
69 | return it
70 | } ?: getDummyBlogPost()
71 | }
72 | }
73 |
74 | @FlowPreview
75 | @ExperimentalCoroutinesApi
76 | fun getDummyBlogPost(): BlogPost {
77 | return BlogPost(-1, "", "", "", "", "", "")
78 | }
79 |
80 | @FlowPreview
81 | @ExperimentalCoroutinesApi
82 | fun BlogViewModel.getUpdatedBlogUri(): Uri? {
83 | getCurrentViewStateOrNew().let {
84 | it.updatedBlogFields.updatedImageUri?.let { string ->
85 | return string.toUri()
86 | }
87 | }
88 | return null
89 | }
90 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.base
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.fragment.app.Fragment
8 | import androidx.navigation.fragment.findNavController
9 | import androidx.navigation.ui.AppBarConfiguration
10 | import androidx.navigation.ui.NavigationUI
11 | import com.mi.mvi.common.DataStateChangeListener
12 | import com.mi.mvi.common.UICommunicationListener
13 | import com.mi.mvi.domain.datastate.DataState
14 | import com.mi.mvi.domain.datastate.MessageType
15 | import com.mi.mvi.domain.datastate.StateMessage
16 | import com.mi.mvi.domain.datastate.UIComponentType
17 | import com.mi.mvi.domain.viewstate.CreateBlogViewState
18 | import com.mi.mvi.features.main.MainActivity
19 | import com.mi.mvi.features.main.blog.BlogFragment
20 | import kotlinx.coroutines.ExperimentalCoroutinesApi
21 | import kotlinx.coroutines.FlowPreview
22 |
23 | @FlowPreview
24 | @ExperimentalCoroutinesApi
25 | abstract class BaseFragment(private val contentLayoutId: Int) : Fragment(contentLayoutId) {
26 |
27 | protected var dataStateChangeListener: DataStateChangeListener? = null
28 | protected var uiCommunicationListener: UICommunicationListener? = null
29 |
30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
31 | super.onViewCreated(view, savedInstanceState)
32 | setupActionBarWithNavController(activity = activity as AppCompatActivity)
33 | }
34 |
35 | private fun setupActionBarWithNavController(activity: AppCompatActivity) {
36 | if (activity is MainActivity && this !is BlogFragment) {
37 | val appBarConfiguration = AppBarConfiguration(setOf(contentLayoutId))
38 | NavigationUI.setupActionBarWithNavController(
39 | activity as AppCompatActivity,
40 | findNavController(),
41 | appBarConfiguration
42 | )
43 | }
44 | }
45 |
46 | override fun onAttach(context: Context) {
47 | super.onAttach(context)
48 | try {
49 | dataStateChangeListener = context as DataStateChangeListener
50 | uiCommunicationListener = context as UICommunicationListener
51 | } catch (e: ClassCastException) {
52 | }
53 | }
54 |
55 | fun showErrorDialog(errorMessage: String) {
56 | dataStateChangeListener?.onDataStateChangeListener(
57 | DataState.ERROR(
58 | StateMessage(
59 | errorMessage,
60 | UIComponentType.DIALOG,
61 | MessageType.ERROR
62 | )
63 | )
64 | )
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/features/src/main/res/navigation/nav_blog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
20 |
21 |
22 |
27 |
34 |
35 |
44 |
45 |
46 |
47 |
52 |
53 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/auth/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.auth
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.lifecycle.Observer
7 | import com.mi.mvi.R
8 | import com.mi.mvi.base.BaseActivity
9 | import com.mi.mvi.domain.datastate.DataState
10 | import com.mi.mvi.events.AuthEventState
11 | import com.mi.mvi.features.main.MainActivity
12 | import com.mi.mvi.mapper.TokenMapper
13 | import kotlinx.android.synthetic.main.activity_splash.*
14 | import kotlinx.coroutines.ExperimentalCoroutinesApi
15 | import kotlinx.coroutines.FlowPreview
16 | import org.koin.androidx.viewmodel.ext.android.viewModel
17 |
18 | @FlowPreview
19 | @ExperimentalCoroutinesApi
20 | class SplashActivity : BaseActivity(R.layout.activity_splash) {
21 |
22 | private val authViewModel: AuthViewModel by viewModel()
23 | private val tokenMapper = TokenMapper()
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | subscribeObservers()
27 | checkPreviousAuthUser()
28 | }
29 |
30 | private fun subscribeObservers() {
31 | authViewModel.dataState.observe(this, Observer { dataState ->
32 | onDataStateChangeListener(dataState)
33 | when (dataState) {
34 | is DataState.SUCCESS -> {
35 | dataState.data?.token?.let { token ->
36 | sessionManager.login(tokenMapper.mapToView(token))
37 | } ?: navLoginActivity()
38 | }
39 | is DataState.ERROR -> {
40 | navLoginActivity()
41 | }
42 | }
43 | })
44 |
45 | sessionManager.cachedTokenViewEntity.observe(
46 | this,
47 | Observer { authToken ->
48 | authToken?.let {
49 | if (it.account_pk != -1 && it.token != null) {
50 | navMainActivity()
51 | } else {
52 | navLoginActivity()
53 | }
54 | }
55 | })
56 | }
57 |
58 | private fun checkPreviousAuthUser() {
59 | authViewModel.setEventState(AuthEventState.CheckTokenEvent)
60 | }
61 |
62 | private fun navMainActivity() {
63 | startActivity(Intent(this, MainActivity::class.java))
64 | finish()
65 | }
66 |
67 | private fun navLoginActivity() {
68 | startActivity(Intent(this, AuthActivity::class.java))
69 | finish()
70 | }
71 |
72 | override fun displayLoading(isLoading: Boolean) {
73 | progress_bar.visibility = if (isLoading) {
74 | View.VISIBLE
75 | } else {
76 | View.GONE
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/account/AccountFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.account
2 |
3 | import android.os.Bundle
4 | import android.view.Menu
5 | import android.view.MenuInflater
6 | import android.view.MenuItem
7 | import android.view.View
8 | import androidx.lifecycle.Observer
9 | import androidx.navigation.fragment.findNavController
10 | import com.mi.mvi.R
11 | import com.mi.mvi.domain.datastate.DataState
12 | import com.mi.mvi.events.AccountEventState
13 | import com.mi.mvi.model.UserView
14 | import kotlinx.android.synthetic.main.fragment_account.*
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.FlowPreview
17 |
18 | @FlowPreview
19 | @ExperimentalCoroutinesApi
20 | class AccountFragment : BaseAccountFragment(R.layout.fragment_account) {
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | setHasOptionsMenu(true)
25 |
26 | subscribeObservers()
27 | viewModel.setEventState(AccountEventState.GetAccountEvent)
28 |
29 | change_password.setOnClickListener { findNavController().navigate(R.id.action_accountFragment_to_changePasswordFragment) }
30 | logout_button.setOnClickListener { viewModel.logout() }
31 | }
32 |
33 | private fun subscribeObservers() {
34 | viewModel.dataState.observe(viewLifecycleOwner, Observer { dataState ->
35 | dataStateChangeListener?.onDataStateChangeListener(dataState)
36 | when (dataState) {
37 | is DataState.SUCCESS -> {
38 | dataState.data?.let { viewState ->
39 | viewState.user?.let { account ->
40 | viewModel.setAccountData(account)
41 | }
42 | }
43 | }
44 | }
45 | })
46 |
47 | viewModel.viewState.observe(viewLifecycleOwner, Observer {
48 | it.user?.let { account ->
49 | setAccountAccount(userMapper.mapToView(account))
50 | }
51 | })
52 | }
53 |
54 | private fun setAccountAccount(account: UserView) {
55 | email.text = account.email
56 | username.text = account.username
57 | }
58 |
59 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
60 | super.onCreateOptionsMenu(menu, inflater)
61 | inflater.inflate(R.menu.edit_view_menu, menu)
62 | }
63 |
64 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
65 | when (item.itemId) {
66 | R.id.edit -> {
67 | findNavController().navigate(R.id.action_accountFragment_to_updateAccountFragment)
68 | return true
69 | }
70 | }
71 | return super.onOptionsItemSelected(item)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/features/src/main/res/navigation/nav_account.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
21 |
22 |
29 |
30 |
31 |
36 |
37 |
46 |
47 |
48 |
49 |
50 |
55 |
56 |
57 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/fragment_update_account.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
49 |
50 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/source/BlogRemoteDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.source
2 |
3 | import com.mi.mvi.data.datasource.remote.BlogRemoteDataSource
4 | import com.mi.mvi.data.entity.BaseEntity
5 | import com.mi.mvi.data.entity.BlogPostEntity
6 | import com.mi.mvi.data.entity.BlogPostListEntity
7 | import com.mi.mvi.remote.mapper.BaseEntityMapper
8 | import com.mi.mvi.remote.mapper.BlogPostEntityMapper
9 | import com.mi.mvi.remote.mapper.BlogPostListEntityMapper
10 | import com.mi.mvi.remote.service.BlogAPIService
11 | import okhttp3.MultipartBody
12 | import okhttp3.RequestBody
13 |
14 | class BlogRemoteDataSourceImpl(
15 | private val blogAPIService: BlogAPIService,
16 | private val baseEntityMapper: BaseEntityMapper,
17 | private val blogPostEntityMapper: BlogPostEntityMapper,
18 | private val blogPostListEntityMapper: BlogPostListEntityMapper
19 |
20 | ) : BlogRemoteDataSource {
21 |
22 | override suspend fun searchListBlogPosts(
23 | authorization: String,
24 | query: String,
25 | ordering: String,
26 | page: Int
27 | ): BlogPostListEntity {
28 | return blogPostListEntityMapper.mapFromRemote(
29 | blogAPIService.searchListBlogPosts(
30 | authorization,
31 | query,
32 | ordering,
33 | page
34 | )
35 | )
36 | }
37 |
38 | override suspend fun isAuthorOfBlogPost(authorization: String, slug: String): BaseEntity {
39 | return baseEntityMapper.mapFromRemote(
40 | blogAPIService.isAuthorOfBlogPost(
41 | authorization,
42 | slug
43 | )
44 | )
45 | }
46 |
47 | override suspend fun deleteBlogPost(authorization: String, slug: String?): BaseEntity {
48 | return baseEntityMapper.mapFromRemote(blogAPIService.deleteBlogPost(authorization, slug))
49 | }
50 |
51 | override suspend fun updateBlog(
52 | authorization: String,
53 | slug: String,
54 | title: RequestBody,
55 | body: RequestBody,
56 | image: MultipartBody.Part?
57 | ): BlogPostEntity {
58 | return blogPostEntityMapper.mapFromRemote(
59 | blogAPIService.updateBlog(
60 | authorization,
61 | slug,
62 | title,
63 | body,
64 | image
65 | )
66 | )
67 | }
68 |
69 | override suspend fun createBlog(
70 | authorization: String,
71 | title: RequestBody,
72 | body: RequestBody,
73 | image: MultipartBody.Part?
74 | ): BlogPostEntity {
75 | return blogPostEntityMapper.mapFromRemote(
76 | blogAPIService.createBlog(
77 | authorization,
78 | title,
79 | body,
80 | image
81 | )
82 | )
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
36 |
37 |
47 |
48 |
49 |
50 |
51 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/mi/mvi/remote/koin/RemoteModule.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.remote.koin
2 |
3 | import com.mi.mvi.data.datasource.remote.AccountRemoteDataSource
4 | import com.mi.mvi.data.datasource.remote.AuthRemoteDataSource
5 | import com.mi.mvi.data.datasource.remote.BlogRemoteDataSource
6 | import com.mi.mvi.remote.mapper.BaseEntityMapper
7 | import com.mi.mvi.remote.mapper.BlogPostEntityMapper
8 | import com.mi.mvi.remote.mapper.BlogPostListEntityMapper
9 | import com.mi.mvi.remote.mapper.UserEntityMapper
10 | import com.mi.mvi.remote.service.AccountAPIService
11 | import com.mi.mvi.remote.service.AuthAPIService
12 | import com.mi.mvi.remote.service.BlogAPIService
13 | import com.mi.mvi.remote.source.AccountRemoteDataSourceImpl
14 | import com.mi.mvi.remote.source.AuthRemoteDataSourceImpl
15 | import com.mi.mvi.remote.source.BlogRemoteDataSourceImpl
16 | import java.util.concurrent.TimeUnit
17 | import okhttp3.OkHttpClient
18 | import okhttp3.logging.HttpLoggingInterceptor
19 | import org.koin.dsl.module
20 | import retrofit2.Retrofit
21 | import retrofit2.converter.gson.GsonConverterFactory
22 |
23 | const val BASE_URL = "https://open-api.xyz/api/"
24 |
25 | val remoteModule = module {
26 |
27 | single {
28 | val logging = HttpLoggingInterceptor()
29 | logging.level = HttpLoggingInterceptor.Level.BODY
30 | val httpClient = OkHttpClient.Builder()
31 | httpClient.readTimeout(6, TimeUnit.SECONDS)
32 | httpClient.writeTimeout(6, TimeUnit.SECONDS)
33 | httpClient.connectTimeout(6, TimeUnit.SECONDS)
34 | httpClient.addInterceptor(logging)
35 |
36 | Retrofit.Builder()
37 | .baseUrl(BASE_URL)
38 | .addConverterFactory(GsonConverterFactory.create())
39 | .client(httpClient.build())
40 | .build()
41 | }
42 |
43 | factory { BaseEntityMapper() }
44 | factory { UserEntityMapper() }
45 | factory { BlogPostEntityMapper() }
46 | factory { BlogPostListEntityMapper() }
47 |
48 | factory { provideAuthAPI(get()) }
49 | factory { provideAccountAPI(get()) }
50 | factory { provideBlogAPI(get()) }
51 |
52 | factory {
53 | AuthRemoteDataSourceImpl(
54 | get(),
55 | get()
56 | )
57 | }
58 |
59 | factory {
60 | AccountRemoteDataSourceImpl(
61 | get(),
62 | get(),
63 | get()
64 | )
65 | }
66 | factory {
67 | BlogRemoteDataSourceImpl(
68 | get(), get(), get(), get()
69 | )
70 | }
71 | }
72 |
73 | fun provideAuthAPI(retrofit: Retrofit): AuthAPIService =
74 | retrofit.create(AuthAPIService::class.java)
75 |
76 | fun provideAccountAPI(retrofit: Retrofit): AccountAPIService =
77 | retrofit.create(AccountAPIService::class.java)
78 |
79 | fun provideBlogAPI(retrofit: Retrofit): BlogAPIService =
80 | retrofit.create(BlogAPIService::class.java)
81 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/mi/mvi/domain/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.domain
2 |
3 | class Constants {
4 | companion object {
5 |
6 | // Shared Preference Keys
7 |
8 | // -----------------------------NETWORK-----------------------
9 | const val PASSWORD_RESET_URL: String = "https://open-api.xyz/password_reset/"
10 | const val NETWORK_TIMEOUT = 6000L
11 | const val CACHE_TIMEOUT = 2000L
12 |
13 | // -----------------------------SUCCESS-----------------------
14 | const val SUCCESS = "success"
15 | const val DELETE = "deleted"
16 | const val RESPONSE_PASSWORD_UPDATE_SUCCESS = "successfully changed password"
17 | const val RESPONSE_CHECK_PREVIOUS_AUTH_USER_DONE =
18 | "Done checking for previously authenticated user."
19 | const val RESPONSE_MUST_BECOME_CODINGWITHMITCH_MEMBER =
20 | "You must become a member on Codingwithmitch.com to access the API. Visit https://codingwithmitch.com/enroll/"
21 | const val RESPONSE_NO_PERMISSION_TO_EDIT = "You don't have permission to edit that."
22 | const val RESPONSE_HAS_PERMISSION_TO_EDIT = "You have permission to edit that."
23 | const val SUCCESS_BLOG_CREATED = "created"
24 | const val SUCCESS_BLOG_DELETED = "deleted"
25 | const val SUCCESS_BLOG_UPDATED = "updated"
26 |
27 | // -----------------------------ERROR-----------------------
28 | const val UNABLE_TO_RESOLVE_HOST = "Unable to resolve host"
29 | const val UNABLE_TODO_OPERATION_WO_INTERNET =
30 | "Can't do that operation without an internet connection"
31 |
32 | const val ERROR_SAVE_ACCOUNT_PROPERTIES =
33 | "Error saving account properties.\nTry restarting the app."
34 | const val ERROR_SAVE_AUTH_TOKEN =
35 | "Error saving authentication token.\nTry restarting the app."
36 | const val ERROR_SOMETHING_WRONG_WITH_IMAGE = "Something went wrong with the image."
37 | const val ERROR_MUST_SELECT_IMAGE = "You must select an image."
38 |
39 | const val GENERIC_AUTH_ERROR = "Error"
40 | const val INVALID_PAGE = "Invalid page."
41 | const val ERROR_CHECK_NETWORK_CONNECTION = "Check network connection."
42 | const val ERROR_UNKNOWN = "Unknown error"
43 | const val INVALID_CREDENTIALS = "Invalid credentials"
44 | const val SOMETHING_WRONG_WITH_IMAGE = "Something went wrong with the image."
45 | const val INVALID_STATE_EVENT = "Invalid state event"
46 | const val CANNOT_BE_UNDONE = "This can't be undone."
47 | const val NETWORK_ERROR = "Network error"
48 | const val NETWORK_ERROR_TIMEOUT = "Network timeout"
49 | const val CACHE_ERROR_TIMEOUT = "Cache timeout"
50 | const val UNKNOWN_ERROR = "Unknown error"
51 | const val ERROR_ALL_FIELDS_ARE_REQUIRED = "All fields are required"
52 | const val ERROR_PASSWORD_DOESNOT_MATCH = "Password and confirm password must be same"
53 | const val INVALID_PAGE_NUMBER = "invalid page number"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/layout_blog_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
18 |
19 |
20 |
31 |
32 |
33 |
43 |
44 |
51 |
52 |
60 |
61 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/data/src/main/java/com/mi/mvi/data/repository/CreateBlogRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.data.repository
2 |
3 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource
4 | import com.mi.mvi.data.datasource.remote.BlogRemoteDataSource
5 | import com.mi.mvi.data.entity.BlogPostEntity
6 | import com.mi.mvi.domain.Constants.Companion.SUCCESS_BLOG_CREATED
7 | import com.mi.mvi.domain.datastate.DataState
8 | import com.mi.mvi.domain.datastate.MessageType
9 | import com.mi.mvi.domain.datastate.StateMessage
10 | import com.mi.mvi.domain.datastate.UIComponentType
11 | import com.mi.mvi.domain.model.Token
12 | import com.mi.mvi.domain.repository.CreateBlogRepository
13 | import com.mi.mvi.domain.viewstate.CreateBlogViewState
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.flow.Flow
17 | import okhttp3.MultipartBody
18 | import okhttp3.RequestBody
19 |
20 | @ExperimentalCoroutinesApi
21 | class CreateBlogRepositoryImpl(
22 | private val blogRemoteDataSource: BlogRemoteDataSource,
23 | private val blogCacheDataSource: BlogCacheDataSource
24 | ) : CreateBlogRepository {
25 | override fun createNewBlogPost(
26 | token: Token,
27 | title: RequestBody,
28 | body: RequestBody,
29 | image: MultipartBody.Part
30 | ): Flow> {
31 | return object :
32 | NetworkBoundResource(
33 | Dispatchers.IO,
34 | apiCall = {
35 | blogRemoteDataSource.createBlog(
36 | "Token ${token.token}",
37 | title,
38 | body,
39 | image
40 | )
41 | }) {
42 |
43 | override suspend fun updateCache(networkObject: BlogPostEntity) {
44 | if (networkObject.response == SUCCESS_BLOG_CREATED) {
45 | val updateBlogPost =
46 | BlogPostEntity(
47 | networkObject.pk,
48 | networkObject.title,
49 | networkObject.slug,
50 | networkObject.body,
51 | networkObject.image,
52 | networkObject.date_updated,
53 | networkObject.username
54 | )
55 | blogCacheDataSource.insert(updateBlogPost)
56 | }
57 | }
58 |
59 | override suspend fun handleNetworkSuccess(response: BlogPostEntity): DataState? {
60 | return if (response.response == SUCCESS_BLOG_CREATED) {
61 | DataState.ERROR(
62 | StateMessage(
63 | SUCCESS_BLOG_CREATED,
64 | UIComponentType.DIALOG,
65 | MessageType.SUCCESS
66 | )
67 | )
68 | } else null
69 | }
70 | }.result
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/create_blog/CreateBlogViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.create_blog
2 |
3 | import android.net.Uri
4 | import androidx.core.net.toUri
5 | import com.mi.mvi.base.BaseViewModel
6 | import com.mi.mvi.common.SessionManager
7 | import com.mi.mvi.domain.datastate.DataState
8 | import com.mi.mvi.domain.usecase.blogs.CreateBlogUseCase
9 | import com.mi.mvi.domain.viewstate.CreateBlogViewState
10 | import com.mi.mvi.domain.viewstate.NewBlogFields
11 | import com.mi.mvi.events.CreateBlogEventState
12 | import com.mi.mvi.events.CreateBlogEventState.CreateNewBlogEvent
13 | import com.mi.mvi.mapper.TokenMapper
14 | import kotlinx.coroutines.ExperimentalCoroutinesApi
15 | import kotlinx.coroutines.FlowPreview
16 | import kotlinx.coroutines.flow.Flow
17 | import kotlinx.coroutines.flow.emitAll
18 | import kotlinx.coroutines.flow.flow
19 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
20 | import okhttp3.RequestBody.Companion.toRequestBody
21 |
22 | @FlowPreview
23 | @ExperimentalCoroutinesApi
24 | class CreateBlogViewModel(
25 | private val createBlogUseCase: CreateBlogUseCase,
26 | private val sessionManager: SessionManager,
27 | private val tokenMapper: TokenMapper
28 |
29 | ) : BaseViewModel() {
30 |
31 | override fun handleEventState(eventState: CreateBlogEventState): Flow> =
32 | flow {
33 | when (eventState) {
34 | is CreateNewBlogEvent -> {
35 | sessionManager.cachedTokenViewEntity.value?.let { authToken ->
36 | val title = eventState.title
37 | .toRequestBody("text/plain".toMediaTypeOrNull())
38 | val body = eventState.body
39 | .toRequestBody("text/plain".toMediaTypeOrNull())
40 |
41 | emitAll(
42 | createBlogUseCase.invoke(
43 | tokenMapper.mapFromView(authToken),
44 | title,
45 | body,
46 | eventState.image
47 | )
48 | )
49 | }
50 | }
51 | }
52 | }
53 |
54 | override fun initNewViewState(): CreateBlogViewState {
55 | return CreateBlogViewState()
56 | }
57 |
58 | fun setNewBlogFields(title: String?, body: String?, uri: Uri?) {
59 | val update = getCurrentViewStateOrNew()
60 | val newBlogFields = update.newBlogField
61 | title?.let { newBlogFields.newBlogTitle = it }
62 | body?.let { newBlogFields.newBlogBody = it }
63 | uri?.let { newBlogFields.newImageUri = it.toString() }
64 | update.newBlogField = newBlogFields
65 | setViewState(update)
66 | }
67 |
68 | fun clearNewBlogFields() {
69 | val update = getCurrentViewStateOrNew()
70 | update.newBlogField =
71 | NewBlogFields()
72 | setViewState(update)
73 | }
74 |
75 | fun getNewImageUri(): Uri? {
76 | getCurrentViewStateOrNew().let { viewState ->
77 | viewState.newBlogField.let { newBlogFields ->
78 | return newBlogFields.newImageUri?.toUri()
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/mi/mvi/cache/db/BlogPostDao.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.cache.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.mi.mvi.cache.model.CachedBlogPost
8 | import com.mi.mvi.data.datasource.cache.BlogCacheDataSource.Companion.PAGINATION_PAGE_SIZE
9 |
10 | @Dao
11 | interface BlogPostDao {
12 | @Insert(onConflict = OnConflictStrategy.REPLACE)
13 | suspend fun insert(cachedBlogPost: CachedBlogPost): Long
14 |
15 | @Query(
16 | """
17 | DELETE FROM blog_post WHERE pk = :pk
18 | """
19 | )
20 | suspend fun deleteBlogPost(pk: Int)
21 |
22 | @Query(
23 | """
24 | UPDATE blog_post SET title = :title, body = :body, image = :image
25 | WHERE pk = :pk
26 | """
27 | )
28 | suspend fun updateBlogPost(pk: Int, title: String?, body: String?, image: String?)
29 |
30 | @Query(
31 | """
32 | SELECT * FROM blog_post
33 | WHERE title LIKE '%' || :query || '%'
34 | OR body LIKE '%' || :query || '%'
35 | OR username LIKE '%' || :query || '%'
36 | LIMIT (:page * :pageSize)
37 | """
38 | )
39 | suspend fun getAllBlogPosts(
40 | query: String,
41 | page: Int,
42 | pageSize: Int = PAGINATION_PAGE_SIZE
43 | ): MutableList
44 |
45 | @Query(
46 | """
47 | SELECT * FROM blog_post
48 | WHERE title LIKE '%' || :query || '%'
49 | OR body LIKE '%' || :query || '%'
50 | OR username LIKE '%' || :query || '%'
51 | ORDER BY date_updated DESC LIMIT (:page * :pageSize)
52 | """
53 | )
54 | suspend fun searchBlogPostsOrderByDateDESC(
55 | query: String,
56 | page: Int,
57 | pageSize: Int = PAGINATION_PAGE_SIZE
58 | ): MutableList
59 |
60 | @Query(
61 | """
62 | SELECT * FROM blog_post
63 | WHERE title LIKE '%' || :query || '%'
64 | OR body LIKE '%' || :query || '%'
65 | OR username LIKE '%' || :query || '%'
66 | ORDER BY date_updated ASC LIMIT (:page * :pageSize)"""
67 | )
68 | suspend fun searchBlogPostsOrderByDateASC(
69 | query: String,
70 | page: Int,
71 | pageSize: Int = PAGINATION_PAGE_SIZE
72 | ): MutableList
73 |
74 | @Query(
75 | """
76 | SELECT * FROM blog_post
77 | WHERE title LIKE '%' || :query || '%'
78 | OR body LIKE '%' || :query || '%'
79 | OR username LIKE '%' || :query || '%'
80 | ORDER BY username DESC LIMIT (:page * :pageSize)"""
81 | )
82 | suspend fun searchBlogPostsOrderByAuthorDESC(
83 | query: String,
84 | page: Int,
85 | pageSize: Int = PAGINATION_PAGE_SIZE
86 | ): MutableList
87 |
88 | @Query(
89 | """
90 | SELECT * FROM blog_post
91 | WHERE title LIKE '%' || :query || '%'
92 | OR body LIKE '%' || :query || '%'
93 | OR username LIKE '%' || :query || '%'
94 | ORDER BY username ASC LIMIT (:page * :pageSize)
95 | """
96 | )
97 | suspend fun searchBlogPostsOrderByAuthorASC(
98 | query: String,
99 | page: Int,
100 | pageSize: Int = PAGINATION_PAGE_SIZE
101 | ): MutableList
102 | }
103 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.base
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import android.view.inputmethod.InputMethodManager
7 | import androidx.annotation.LayoutRes
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.core.app.ActivityCompat
10 | import androidx.core.content.ContextCompat
11 | import com.mi.mvi.common.*
12 | import com.mi.mvi.common.SessionManager
13 | import com.mi.mvi.domain.datastate.DataState
14 | import com.mi.mvi.domain.datastate.StateMessage
15 | import com.mi.mvi.domain.datastate.UIComponentType
16 | import com.mi.mvi.utils.Constants.Companion.PERMISSION_REQUEST_READ_STORAGE
17 | import org.koin.android.ext.android.inject
18 |
19 | abstract class BaseActivity(@LayoutRes contentLayoutId: Int) : AppCompatActivity(contentLayoutId),
20 | DataStateChangeListener,
21 | UICommunicationListener {
22 |
23 | val sessionManager: SessionManager by inject()
24 |
25 | override fun onUIMessageReceived(stateMessage: StateMessage) {
26 | when (stateMessage.uiComponentType) {
27 | is UIComponentType.AreYouSureDialog -> {
28 | areYouSureDialog(stateMessage.message, (stateMessage.uiComponentType as UIComponentType.AreYouSureDialog).callBack)
29 | }
30 | is UIComponentType.DIALOG -> {
31 | displayInfoDialog(stateMessage.message)
32 | }
33 | is UIComponentType.TOAST -> {
34 | displayToast(stateMessage.message)
35 | }
36 | is UIComponentType.NONE -> {
37 | }
38 | }
39 | }
40 |
41 | override fun onDataStateChangeListener(dataState: DataState<*>?) {
42 | dataState?.let {
43 | displayLoading(it.loading)
44 | it.stateMessage?.let { stateMessage ->
45 | handleResponseState(stateMessage)
46 | }
47 | }
48 | }
49 |
50 | private fun handleResponseState(stateMessage: StateMessage?) {
51 | stateMessage?.message?.let { message ->
52 | when (stateMessage.uiComponentType) {
53 | is UIComponentType.DIALOG -> {
54 | displayErrorDialog(message)
55 | }
56 | is UIComponentType.TOAST -> {
57 | displayToast(message)
58 | }
59 | }
60 | }
61 | }
62 |
63 | override fun hideSoftKeyboard() {
64 | currentFocus?.let { currentFocus ->
65 | val inputMethodManager = getSystemService(
66 | Context.INPUT_METHOD_SERVICE
67 | ) as InputMethodManager
68 |
69 | inputMethodManager.hideSoftInputFromWindow(currentFocus.windowToken, 0)
70 | }
71 | }
72 |
73 | override fun isStoragePermissionGranted(): Boolean {
74 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
75 | != PackageManager.PERMISSION_GRANTED &&
76 | ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
77 | != PackageManager.PERMISSION_GRANTED
78 | ) {
79 | ActivityCompat.requestPermissions(
80 | this,
81 | arrayOf(
82 | Manifest.permission.READ_EXTERNAL_STORAGE,
83 | Manifest.permission.WRITE_EXTERNAL_STORAGE
84 | ), PERMISSION_REQUEST_READ_STORAGE
85 | )
86 | return false
87 | } else {
88 | return true
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/features/src/main/java/com/mi/mvi/features/main/account/AccountViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mi.mvi.features.main.account
2 |
3 | import com.mi.mvi.base.BaseViewModel
4 | import com.mi.mvi.common.SessionManager
5 | import com.mi.mvi.domain.datastate.DataState
6 | import com.mi.mvi.domain.model.User
7 | import com.mi.mvi.domain.usecase.account.ChangePasswordUseCase
8 | import com.mi.mvi.domain.usecase.account.GetAccountUseCase
9 | import com.mi.mvi.domain.usecase.account.UpdateAccountUseCase
10 | import com.mi.mvi.domain.viewstate.AccountViewState
11 | import com.mi.mvi.events.AccountEventState
12 | import com.mi.mvi.events.AccountEventState.*
13 | import com.mi.mvi.mapper.TokenMapper
14 | import com.mi.mvi.mapper.UserMapper
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.FlowPreview
17 | import kotlinx.coroutines.flow.Flow
18 | import kotlinx.coroutines.flow.emitAll
19 | import kotlinx.coroutines.flow.flow
20 |
21 | @FlowPreview
22 | @ExperimentalCoroutinesApi
23 | class AccountViewModel(
24 | private val sessionManager: SessionManager,
25 | private val accountUseCase: GetAccountUseCase,
26 | private val updateAccountUseCase: UpdateAccountUseCase,
27 | private val changePasswordUseCase: ChangePasswordUseCase,
28 | private val tokenMapper: TokenMapper,
29 | private val userMapper: UserMapper
30 | ) : BaseViewModel() {
31 |
32 | override fun handleEventState(eventState: AccountEventState): Flow> =
33 | flow {
34 | when (eventState) {
35 | is GetAccountEvent -> {
36 | sessionManager.cachedTokenViewEntity.value?.let {
37 | emitAll(
38 | accountUseCase.invoke(
39 | tokenMapper.mapFromView(it)
40 | )
41 | )
42 | }
43 | }
44 | is UpdateAccountEvent -> {
45 | sessionManager.cachedTokenViewEntity.value?.let { authToken ->
46 | authToken.account_pk?.let { pk ->
47 | emitAll(
48 | updateAccountUseCase.invoke(
49 | tokenMapper.mapFromView(authToken),
50 | User(
51 | pk,
52 | eventState.email,
53 | eventState.username,
54 | null
55 | )
56 | )
57 | )
58 | }
59 | }
60 | }
61 | is ChangePasswordEvent -> {
62 | sessionManager.cachedTokenViewEntity.value?.let { authToken ->
63 | emitAll(
64 | changePasswordUseCase.invoke(
65 | tokenMapper.mapFromView(authToken),
66 | eventState.currentPassword,
67 | eventState.newPassword,
68 | eventState.confirmNewPassword
69 | )
70 | )
71 | }
72 | }
73 | }
74 | }
75 |
76 | override fun initNewViewState(): AccountViewState {
77 | return AccountViewState()
78 | }
79 |
80 | fun setAccountData(user: User) {
81 | val update = getCurrentViewStateOrNew()
82 | if (update.user != user) {
83 | update.user = user
84 | _viewState.value = update
85 | }
86 | }
87 |
88 | fun logout() {
89 | sessionManager.logout()
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/features/src/main/res/layout/layout_blog_filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
22 |
23 |
29 |
30 |
38 |
39 |
47 |
48 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
73 |
74 |
82 |
83 |
84 |
85 |
91 |
92 |
101 |
102 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------