├── 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 | 4 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /features/src/main/res/menu/update_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /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 | [![kotlin](https://img.shields.io/badge/Kotlin-1.3.xxx-blue)](https://kotlinlang.org/) [![MVI ](https://img.shields.io/badge/Architecture-MVI-brightgreen)](https://www.youtube.com/watch?v=tIPxSWx5qpk) [![coroutines](https://img.shields.io/badge/Coroutines-Asynchronous-red)](https://developer.android.com/kotlin/coroutines) [![Kotlin-Android-Extensions ](https://img.shields.io/badge/Kotlin--Android--Extensions-plugin-red.svg)](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 | --------------------------------------------------------------------------------