├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── font │ │ │ │ ├── poppins_black.ttf │ │ │ │ ├── poppins_bold.ttf │ │ │ │ ├── poppins_light.ttf │ │ │ │ ├── poppins_medium.ttf │ │ │ │ └── poppins_regular.ttf │ │ │ ├── drawable │ │ │ │ ├── il_placeholder.png │ │ │ │ ├── bg_image_button.xml │ │ │ │ ├── text_button.xml │ │ │ │ ├── ic_add.xml │ │ │ │ ├── bg_button.xml │ │ │ │ ├── ic_exit.xml │ │ │ │ ├── bg_edit_text_error.xml │ │ │ │ ├── ic_map.xml │ │ │ │ ├── bg_edit_text.xml │ │ │ │ ├── ic_login.xml │ │ │ │ ├── ic_message.xml │ │ │ │ ├── ic_language.xml │ │ │ │ ├── ic_lock.xml │ │ │ │ ├── ic_account.xml │ │ │ │ ├── ic_password.xml │ │ │ │ ├── il_logo.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── xml │ │ │ │ ├── file_paths.xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values │ │ │ │ ├── attrs.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v33 │ │ │ │ └── ic_launcher.xml │ │ │ ├── layout │ │ │ │ ├── activity_welcome.xml │ │ │ │ ├── widget_progress_button.xml │ │ │ │ ├── item_loading.xml │ │ │ │ ├── activity_maps.xml │ │ │ │ ├── item_story.xml │ │ │ │ ├── activity_login.xml │ │ │ │ ├── activity_story_detail.xml │ │ │ │ ├── activity_add_story.xml │ │ │ │ ├── activity_register.xml │ │ │ │ └── activity_story.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── values-in-rID │ │ │ │ └── strings.xml │ │ │ └── raw │ │ │ │ ├── map_style.json │ │ │ │ └── lottie_loader.json │ │ ├── java │ │ │ └── com │ │ │ │ └── mirz │ │ │ │ └── storyapp │ │ │ │ ├── data │ │ │ │ ├── response │ │ │ │ │ ├── GeneralResponse.kt │ │ │ │ │ ├── StoryDetailResponse.kt │ │ │ │ │ ├── StoryListResponse.kt │ │ │ │ │ ├── LoginResponse.kt │ │ │ │ │ └── StoryResponse.kt │ │ │ │ ├── source │ │ │ │ │ ├── database │ │ │ │ │ │ ├── RemoteKeys.kt │ │ │ │ │ │ ├── RemoteKeysDao.kt │ │ │ │ │ │ ├── StoryDao.kt │ │ │ │ │ │ └── StoryDatabase.kt │ │ │ │ │ ├── remote │ │ │ │ │ │ ├── AuthInterceptor.kt │ │ │ │ │ │ ├── RetrofitBuilder.kt │ │ │ │ │ │ └── ApiServices.kt │ │ │ │ │ └── local │ │ │ │ │ │ └── UserPreferenceImpl.kt │ │ │ │ ├── repository │ │ │ │ │ ├── AuthRepositoryImpl.kt │ │ │ │ │ └── StoryRepositoryImpl.kt │ │ │ │ └── paging │ │ │ │ │ ├── StoryPagingSource.kt │ │ │ │ │ └── StoryRemoteMediator.kt │ │ │ │ ├── domain │ │ │ │ ├── contract │ │ │ │ │ ├── LogoutUseCaseContract.kt │ │ │ │ │ ├── GetUserUseCaseContract.kt │ │ │ │ │ ├── LoginUseCaseContract.kt │ │ │ │ │ ├── RegisterUseCaseContract.kt │ │ │ │ │ ├── GetStoriesUseCaseContract.kt │ │ │ │ │ ├── GetStoryDetailUseCaseContract.kt │ │ │ │ │ ├── GetStoriesLocationUseCaseContract.kt │ │ │ │ │ └── AddStoryUseCaseContract.kt │ │ │ │ ├── entity │ │ │ │ │ ├── UserEntity.kt │ │ │ │ │ └── StoryEntity.kt │ │ │ │ ├── interfaces │ │ │ │ │ ├── UserPreferenceRepository.kt │ │ │ │ │ ├── AuthRepository.kt │ │ │ │ │ └── StoryRepository.kt │ │ │ │ ├── usecase │ │ │ │ │ ├── LogoutUseCase.kt │ │ │ │ │ ├── GetUserUseCase.kt │ │ │ │ │ ├── GetStoriesUseCase.kt │ │ │ │ │ ├── GetStoryDetailUseCase.kt │ │ │ │ │ ├── AddStoryUseCase.kt │ │ │ │ │ ├── GetStoriesLocationUseCase.kt │ │ │ │ │ ├── RegisterUseCase.kt │ │ │ │ │ └── LoginUseCase.kt │ │ │ │ └── mapper │ │ │ │ │ └── StoryMapper.kt │ │ │ │ ├── utils │ │ │ │ ├── Constant.kt │ │ │ │ ├── ResultState.kt │ │ │ │ └── Extensions.kt │ │ │ │ ├── App.kt │ │ │ │ ├── ui │ │ │ │ ├── login │ │ │ │ │ ├── LoginViewState.kt │ │ │ │ │ ├── LoginViewModel.kt │ │ │ │ │ └── LoginActivity.kt │ │ │ │ ├── add_story │ │ │ │ │ ├── AddStoryViewState.kt │ │ │ │ │ ├── AddStoryViewModel.kt │ │ │ │ │ └── AddStoryActivity.kt │ │ │ │ ├── register │ │ │ │ │ ├── RegisterViewState.kt │ │ │ │ │ ├── RegisterViewModel.kt │ │ │ │ │ └── RegisterActivity.kt │ │ │ │ ├── welcome │ │ │ │ │ ├── WelcomeViewState.kt │ │ │ │ │ ├── WelcomeViewModel.kt │ │ │ │ │ └── WelcomeActivity.kt │ │ │ │ ├── maps │ │ │ │ │ ├── MapsViewState.kt │ │ │ │ │ ├── MapsViewModel.kt │ │ │ │ │ └── MapsActivity.kt │ │ │ │ ├── detail_story │ │ │ │ │ ├── StoryDetailViewState.kt │ │ │ │ │ ├── StoryDetailViewModel.kt │ │ │ │ │ └── StoryDetailActivity.kt │ │ │ │ ├── story │ │ │ │ │ ├── StoryViewState.kt │ │ │ │ │ ├── StoryViewModel.kt │ │ │ │ │ └── StoryActivity.kt │ │ │ │ └── adapter │ │ │ │ │ ├── LoadingStateAdapter.kt │ │ │ │ │ └── StoryAdapter.kt │ │ │ │ ├── widget │ │ │ │ ├── EditText.kt │ │ │ │ └── ProgressButton.kt │ │ │ │ └── Locator.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mirz │ │ │ └── storyapp │ │ │ ├── fake │ │ │ ├── FakeLogoutUseCase.kt │ │ │ ├── FakeGetUserUseCase.kt │ │ │ └── FakeGetStoriesUseCase.kt │ │ │ ├── utils │ │ │ ├── FakeFlowDelegate.kt │ │ │ ├── DataDummy.kt │ │ │ └── MainDispatcherRule.kt │ │ │ ├── ExampleUnitTest.kt │ │ │ └── ui │ │ │ └── story │ │ │ └── StoryViewModelTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mirz │ │ └── storyapp │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/font/poppins_black.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/font/poppins_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/font/poppins_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable/il_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/drawable/il_placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirzaUkas/StoryApp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/response/GeneralResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.response 2 | 3 | data class GeneralResponse( 4 | val error: Boolean, 5 | val message: String 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/LogoutUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | interface LogoutUseCaseContract { 4 | suspend operator fun invoke() 5 | 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/entity/UserEntity.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.entity 2 | 3 | data class UserEntity( 4 | val id: String, 5 | val name: String, 6 | val token: String, 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/utils/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.utils 2 | 3 | object Constant { 4 | const val PREF_ID = "id" 5 | const val PREF_NAME = "name" 6 | const val PREF_TOKEN = "token" 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/response/StoryDetailResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.response 2 | 3 | 4 | data class StoryDetailResponse( 5 | val error: Boolean, 6 | val message: String, 7 | val story: StoryResponse 8 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_image_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/response/StoryListResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.response 2 | 3 | 4 | data class StoryListResponse( 5 | val error: Boolean, 6 | val message: String, 7 | val listStory: List 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/App.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp 2 | 3 | import android.app.Application 4 | 5 | class App : Application() { 6 | override fun onCreate() { 7 | super.onCreate() 8 | Locator.initWith(this) 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/login/LoginViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.login 2 | 3 | import com.mirz.storyapp.utils.ResultState 4 | 5 | data class LoginViewState( 6 | val resultVerifyUser: ResultState = ResultState.Idle() 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/add_story/AddStoryViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.add_story 2 | 3 | import com.mirz.storyapp.utils.ResultState 4 | 5 | data class AddStoryViewState( 6 | val resultAddStory: ResultState = ResultState.Idle() 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/register/RegisterViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.register 2 | 3 | import com.mirz.storyapp.utils.ResultState 4 | 5 | data class RegisterViewState( 6 | val resultRegisterUser: ResultState = ResultState.Idle() 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/welcome/WelcomeViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.welcome 2 | 3 | import com.mirz.storyapp.utils.ResultState 4 | 5 | data class WelcomeViewState( 6 | val resultIsLoggedIn: ResultState = ResultState.Idle() 7 | ) 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 07 21:19:26 WIB 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/fake/FakeLogoutUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.fake 2 | 3 | import com.mirz.storyapp.domain.contract.LogoutUseCaseContract 4 | 5 | class FakeLogoutUseCase : LogoutUseCaseContract { 6 | 7 | 8 | override suspend fun invoke() = Unit 9 | 10 | 11 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/utils/FakeFlowDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.utils 2 | 3 | import kotlinx.coroutines.flow.MutableSharedFlow 4 | 5 | class FakeFlowDelegate { 6 | val flow: MutableSharedFlow = MutableSharedFlow() 7 | 8 | suspend fun emit(value: T) = flow.emit(value) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/GetUserUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import com.mirz.storyapp.domain.entity.UserEntity 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface GetUserUseCaseContract { 7 | operator fun invoke(): Flow 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/entity/StoryEntity.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.entity 2 | 3 | data class StoryEntity( 4 | val id: String, 5 | val name: String, 6 | val description: String, 7 | val photoUrl: String, 8 | val lat: Double, 9 | val lng: Double, 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/maps/MapsViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.maps 2 | 3 | import com.mirz.storyapp.domain.entity.StoryEntity 4 | import com.mirz.storyapp.utils.ResultState 5 | 6 | data class MapsViewState( 7 | val resultStories: ResultState> = ResultState.Idle(), 8 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/detail_story/StoryDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.detail_story 2 | 3 | import com.mirz.storyapp.domain.entity.StoryEntity 4 | import com.mirz.storyapp.utils.ResultState 5 | 6 | data class StoryDetailViewState( 7 | val resultStory: ResultState = ResultState.Idle() 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/LoginUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import com.mirz.storyapp.utils.ResultState 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface LoginUseCaseContract { 7 | operator fun invoke(email: String, password: String): Flow> 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/ui/story/StoryViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.ui.story 2 | 3 | import androidx.paging.PagingData 4 | import com.mirz.storyapp.domain.entity.StoryEntity 5 | 6 | data class StoryViewState( 7 | val resultStories: PagingData = PagingData.empty(), 8 | val username: String = "", 9 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/source/database/RemoteKeys.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.source.database 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "remote_keys") 7 | data class RemoteKeys( 8 | @PrimaryKey val id: String, 9 | val prevKey: Int?, 10 | val nextKey: Int? 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/RegisterUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import com.mirz.storyapp.utils.ResultState 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface RegisterUseCaseContract { 7 | operator fun invoke(name: String, email: String, password: String): Flow> 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/response/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.response 2 | 3 | data class LoginResponse( 4 | val error: Boolean, 5 | val loginResult: LoginResult, 6 | val message: String 7 | ) 8 | 9 | data class LoginResult( 10 | val name: String, 11 | val token: String, 12 | val userId: String 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/GetStoriesUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import androidx.paging.PagingData 4 | import com.mirz.storyapp.domain.entity.StoryEntity 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface GetStoriesUseCaseContract { 8 | operator fun invoke(): Flow> 9 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/GetStoryDetailUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import com.mirz.storyapp.domain.entity.StoryEntity 4 | import com.mirz.storyapp.utils.ResultState 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface GetStoryDetailUseCaseContract { 8 | operator fun invoke(id: String): Flow> 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/GetStoriesLocationUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import com.mirz.storyapp.domain.entity.StoryEntity 4 | import com.mirz.storyapp.utils.ResultState 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface GetStoriesLocationUseCaseContract { 8 | operator fun invoke(): Flow>> 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/interfaces/UserPreferenceRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.interfaces 2 | 3 | import com.mirz.storyapp.domain.entity.UserEntity 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface UserPreferenceRepository { 7 | val userData: Flow 8 | suspend fun saveUser(userEntity: UserEntity) 9 | suspend fun clearUser() 10 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "StoryApp" 16 | include ':app' 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 16dp 5 | 30dp 6 | 40dp 7 | 8 | 12sp 9 | 16sp 10 | 20sp 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/contract/AddStoryUseCaseContract.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.contract 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import com.mirz.storyapp.utils.ResultState 5 | import kotlinx.coroutines.flow.Flow 6 | import java.io.File 7 | 8 | interface AddStoryUseCaseContract { 9 | operator fun invoke(file: File, description: String, latLng: LatLng?): Flow> 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/LogoutUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.mirz.storyapp.domain.contract.LogoutUseCaseContract 4 | import com.mirz.storyapp.domain.interfaces.UserPreferenceRepository 5 | 6 | class LogoutUseCase(private val userPreferenceRepository: UserPreferenceRepository) : 7 | LogoutUseCaseContract { 8 | override suspend fun invoke() = userPreferenceRepository.clearUser() 9 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/utils/ResultState.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.utils 2 | 3 | sealed class ResultState( 4 | val data: T? = null, 5 | val message: String = "", 6 | ) { 7 | class Success(data: T) : ResultState(data) 8 | 9 | class Loading : ResultState() 10 | 11 | class Idle : ResultState() 12 | 13 | class Error(message: String, data: T? = null) : 14 | ResultState(data, message) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/interfaces/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.interfaces 2 | 3 | import com.mirz.storyapp.data.response.GeneralResponse 4 | import com.mirz.storyapp.data.response.LoginResponse 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface AuthRepository { 8 | fun register(email: String, password: String, name: String): Flow 9 | fun login(email: String, password: String): Flow 10 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/fake/FakeGetUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.fake 2 | 3 | import com.mirz.storyapp.domain.contract.GetUserUseCaseContract 4 | import com.mirz.storyapp.domain.entity.UserEntity 5 | import com.mirz.storyapp.utils.FakeFlowDelegate 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class FakeGetUserUseCase : GetUserUseCaseContract { 9 | 10 | val fakeDelegate = FakeFlowDelegate() 11 | 12 | override fun invoke(): Flow = fakeDelegate.flow 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/response/StoryResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.response 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.google.gson.annotations.SerializedName 6 | 7 | 8 | @Entity(tableName = "story") 9 | data class StoryResponse( 10 | @PrimaryKey @field:SerializedName("id") val id: String, 11 | val createdAt: String, 12 | val description: String, 13 | val lat: Double, 14 | val lon: Double, 15 | val name: String, 16 | val photoUrl: String 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/GetUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.mirz.storyapp.domain.contract.GetUserUseCaseContract 4 | import com.mirz.storyapp.domain.entity.UserEntity 5 | import com.mirz.storyapp.domain.interfaces.UserPreferenceRepository 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class GetUserUseCase(private val userPreferenceRepository: UserPreferenceRepository) : 9 | GetUserUseCaseContract { 10 | override fun invoke(): Flow = userPreferenceRepository.userData 11 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_exit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/GetStoriesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.mirz.storyapp.domain.contract.GetStoriesUseCaseContract 4 | import com.mirz.storyapp.domain.interfaces.StoryRepository 5 | import com.mirz.storyapp.domain.mapper.map 6 | import kotlinx.coroutines.flow.map 7 | 8 | class GetStoriesUseCase(private val storyRepository: StoryRepository) : GetStoriesUseCaseContract { 9 | override fun invoke() = storyRepository.getStories().map { pagingData -> 10 | pagingData.map() 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_edit_text_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/fake/FakeGetStoriesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.fake 2 | 3 | import androidx.paging.PagingData 4 | import com.mirz.storyapp.domain.contract.GetStoriesUseCaseContract 5 | import com.mirz.storyapp.domain.entity.StoryEntity 6 | import com.mirz.storyapp.utils.FakeFlowDelegate 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | class FakeGetStoriesUseCase : GetStoriesUseCaseContract { 10 | 11 | val fakeDelegate = FakeFlowDelegate>() 12 | 13 | override fun invoke(): Flow> = fakeDelegate.flow 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/source/database/RemoteKeysDao.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.source.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | 8 | @Dao 9 | interface RemoteKeysDao { 10 | @Insert(onConflict = OnConflictStrategy.REPLACE) 11 | suspend fun insertAll(remoteKey: List) 12 | 13 | @Query("SELECT * FROM remote_keys WHERE id = :id") 14 | suspend fun getRemoteKeysId(id: String): RemoteKeys? 15 | 16 | @Query("DELETE FROM remote_keys") 17 | suspend fun deleteRemoteKeys() 18 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/source/database/StoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.source.database 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.room.Dao 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.mirz.storyapp.data.response.StoryResponse 9 | 10 | @Dao 11 | interface StoryDao { 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | suspend fun insertStories(quote: List) 14 | 15 | @Query("SELECT * FROM story") 16 | fun getAllStories(): PagingSource 17 | 18 | @Query("DELETE FROM story") 19 | suspend fun deleteAll() 20 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/utils/DataDummy.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.utils 2 | 3 | import com.mirz.storyapp.data.response.StoryResponse 4 | 5 | object DataDummy { 6 | 7 | fun generateDummyStoryResponse(): List { 8 | val items: MutableList = arrayListOf() 9 | for (i in 0..100) { 10 | val quote = StoryResponse( 11 | i.toString(), 12 | "createdAt + $i", 13 | "description $i", 14 | 0.0, 15 | 0.0, 16 | "name $i", 17 | "photoUrl $i", 18 | ) 19 | items.add(quote) 20 | } 21 | return items 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mirz/storyapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mirz.storyapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/interfaces/StoryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.interfaces 2 | 3 | import androidx.paging.PagingData 4 | import com.google.android.gms.maps.model.LatLng 5 | import com.mirz.storyapp.data.response.GeneralResponse 6 | import com.mirz.storyapp.data.response.StoryDetailResponse 7 | import com.mirz.storyapp.data.response.StoryListResponse 8 | import com.mirz.storyapp.data.response.StoryResponse 9 | import kotlinx.coroutines.flow.Flow 10 | import java.io.File 11 | 12 | interface StoryRepository { 13 | fun getStories(): Flow> 14 | fun getStory(id: String): Flow 15 | fun addStory(file: File, description: String, latLng: LatLng?): Flow 16 | fun getStoriesLocation(id: Int): Flow 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_edit_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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. 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 -------------------------------------------------------------------------------- /app/src/test/java/com/mirz/storyapp/utils/MainDispatcherRule.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestDispatcher 6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | @ExperimentalCoroutinesApi 13 | class MainDispatcherRule( 14 | private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() 15 | ) : TestWatcher() { 16 | override fun starting(description: Description) { 17 | Dispatchers.setMain(testDispatcher) 18 | } 19 | 20 | override fun finished(description: Description) { 21 | Dispatchers.resetMain() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_login.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_welcome.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #92A3FD 7 | #C58BF2 8 | #9DCEFF 9 | #FF03DAC5 10 | #FF018786 11 | #1D1617 12 | #FF000000 13 | #FFFFFFFF 14 | #FF29B6F6 15 | #FF039BE5 16 | #FFBDBDBD 17 | #FF757575 18 | #F7F8F8 19 | #ADA4A5 20 | #CF212A 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_message.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/GetStoryDetailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.mirz.storyapp.domain.contract.GetStoryDetailUseCaseContract 4 | import com.mirz.storyapp.domain.entity.StoryEntity 5 | import com.mirz.storyapp.domain.interfaces.StoryRepository 6 | import com.mirz.storyapp.domain.mapper.map 7 | import com.mirz.storyapp.utils.ResultState 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.catch 10 | import kotlinx.coroutines.flow.flow 11 | import kotlinx.coroutines.flow.map 12 | 13 | class GetStoryDetailUseCase(private val storyRepository: StoryRepository) : 14 | GetStoryDetailUseCaseContract { 15 | override operator fun invoke(id: String): Flow> = flow { 16 | emit(ResultState.Loading()) 17 | storyRepository.getStory(id).map { 18 | it.story.map() 19 | }.catch { 20 | emit(ResultState.Error(message = it.message.toString())) 21 | }.collect { 22 | emit(ResultState.Success(it)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/AddStoryUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import com.mirz.storyapp.domain.contract.AddStoryUseCaseContract 5 | import com.mirz.storyapp.domain.interfaces.StoryRepository 6 | import com.mirz.storyapp.utils.ResultState 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.flow 10 | import java.io.File 11 | 12 | class AddStoryUseCase(private val storyRepository: StoryRepository) : AddStoryUseCaseContract { 13 | override operator fun invoke( 14 | file: File, 15 | description: String, 16 | latLng: LatLng? 17 | ): Flow> = 18 | flow { 19 | emit(ResultState.Loading()) 20 | storyRepository.addStory(file, description, latLng).catch { 21 | emit(ResultState.Error(message = it.message.toString())) 22 | }.collect { 23 | emit(ResultState.Success(it.message)) 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/source/remote/AuthInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.source.remote 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.stringPreferencesKey 6 | import kotlinx.coroutines.flow.first 7 | import kotlinx.coroutines.runBlocking 8 | import okhttp3.Interceptor 9 | import okhttp3.Response 10 | 11 | class AuthInterceptor(private val dataStore: DataStore) : Interceptor { 12 | 13 | override fun intercept(chain: Interceptor.Chain): Response { 14 | val original = chain.request() 15 | val token = runBlocking { 16 | dataStore.data.first()[stringPreferencesKey("token")] 17 | } 18 | 19 | return if (!token.isNullOrEmpty()) { 20 | val authorized = original.newBuilder() 21 | .addHeader("Authorization", "Bearer $token") 22 | .build() 23 | chain.proceed(authorized) 24 | } else { 25 | chain.proceed(original) 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/GetStoriesLocationUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.mirz.storyapp.domain.contract.GetStoriesLocationUseCaseContract 4 | import com.mirz.storyapp.domain.entity.StoryEntity 5 | import com.mirz.storyapp.domain.interfaces.StoryRepository 6 | import com.mirz.storyapp.domain.mapper.map 7 | import com.mirz.storyapp.utils.ResultState 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.catch 10 | import kotlinx.coroutines.flow.flow 11 | import kotlinx.coroutines.flow.map 12 | 13 | class GetStoriesLocationUseCase(private val storyRepository: StoryRepository) : 14 | GetStoriesLocationUseCaseContract { 15 | 16 | override operator fun invoke(): Flow>> = flow { 17 | emit(ResultState.Loading()) 18 | storyRepository.getStoriesLocation(1).map { 19 | it.listStory.map() 20 | }.catch { 21 | emit(ResultState.Error(message = it.message.toString())) 22 | }.collect { 23 | emit(ResultState.Success(it)) 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_progress_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/mapper/StoryMapper.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.mapper 2 | 3 | import androidx.paging.PagingData 4 | import androidx.paging.map 5 | import com.mirz.storyapp.data.response.StoryResponse 6 | import com.mirz.storyapp.domain.entity.StoryEntity 7 | 8 | 9 | fun StoryResponse.map() = let { story -> 10 | StoryEntity( 11 | id = story.id, 12 | name = story.name, 13 | description = story.description, 14 | photoUrl = story.photoUrl, 15 | lat = story.lat, 16 | lng = story.lon, 17 | ) 18 | } 19 | 20 | fun List.map() = map { story -> 21 | StoryEntity( 22 | id = story.id, 23 | name = story.name, 24 | description = story.description, 25 | photoUrl = story.photoUrl, 26 | lat = story.lat, 27 | lng = story.lon, 28 | ) 29 | } 30 | 31 | fun PagingData.map() = map { story -> 32 | StoryEntity( 33 | id = story.id, 34 | name = story.name, 35 | description = story.description, 36 | photoUrl = story.photoUrl, 37 | lat = story.lat, 38 | lng = story.lon, 39 | ) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/repository/AuthRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.repository 2 | 3 | import com.mirz.storyapp.data.source.remote.ApiServices 4 | import com.mirz.storyapp.domain.interfaces.AuthRepository 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.flow.flow 7 | import kotlinx.coroutines.flow.flowOn 8 | 9 | class AuthRepositoryImpl(private val api: ApiServices) : AuthRepository { 10 | 11 | override fun register(email: String, password: String, name: String) = flow { 12 | emit( 13 | api.register( 14 | hashMapOf( 15 | Pair("name", name), 16 | Pair("password", password), 17 | Pair("email", email), 18 | ) 19 | ) 20 | ) 21 | }.flowOn(Dispatchers.IO) 22 | 23 | override fun login(email: String, password: String) = flow { 24 | emit( 25 | api.login( 26 | hashMapOf( 27 | Pair("password", password), 28 | Pair("email", email), 29 | ) 30 | ) 31 | ) 32 | }.flowOn(Dispatchers.IO) 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/domain/usecase/RegisterUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.domain.usecase 2 | 3 | import com.mirz.storyapp.domain.contract.RegisterUseCaseContract 4 | import com.mirz.storyapp.domain.interfaces.AuthRepository 5 | import com.mirz.storyapp.utils.ResultState 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.flow 9 | 10 | class RegisterUseCase( 11 | private val authRepository: AuthRepository, 12 | ) : RegisterUseCaseContract { 13 | override operator fun invoke( 14 | name: String, 15 | email: String, 16 | password: String 17 | ): Flow> = 18 | flow { 19 | emit(ResultState.Loading()) 20 | authRepository.register( 21 | email, password, name 22 | ).catch { 23 | emit(ResultState.Error(it.message.toString())) 24 | }.collect { result -> 25 | if (result.error) { 26 | emit(ResultState.Error(result.message)) 27 | } else { 28 | emit(ResultState.Success(result.message)) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/source/remote/RetrofitBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.source.remote 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import com.mirz.storyapp.BuildConfig 6 | import okhttp3.OkHttpClient 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | 11 | class RetrofitBuilder(private val dataStore: DataStore) { 12 | 13 | private fun getRetrofit(): Retrofit { 14 | return Retrofit.Builder() 15 | .baseUrl(BuildConfig.BASE_URL) 16 | .client( 17 | OkHttpClient.Builder() 18 | .addInterceptor( 19 | HttpLoggingInterceptor() 20 | .setLevel(HttpLoggingInterceptor.Level.BODY) 21 | ) 22 | .addInterceptor(AuthInterceptor(dataStore)) 23 | .build() 24 | ) 25 | .addConverterFactory(GsonConverterFactory.create()) 26 | .build() 27 | } 28 | 29 | val apiService: ApiServices = getRetrofit().create(ApiServices::class.java) 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mirz/storyapp/data/source/database/StoryDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mirz.storyapp.data.source.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.mirz.storyapp.data.response.StoryResponse 8 | 9 | @Database( 10 | entities = [StoryResponse::class, RemoteKeys::class], 11 | version = 1, 12 | exportSchema = false 13 | ) 14 | abstract class StoryDatabase : RoomDatabase() { 15 | abstract fun storyDao(): StoryDao 16 | abstract fun remoteKeysDao(): RemoteKeysDao 17 | 18 | companion object { 19 | @Volatile 20 | private var INSTANCE: StoryDatabase? = null 21 | 22 | @JvmStatic 23 | fun getDatabase(context: Context): StoryDatabase { 24 | return INSTANCE ?: synchronized(this) { 25 | INSTANCE ?: Room.databaseBuilder( 26 | context.applicationContext, 27 | StoryDatabase::class.java, "story_database" 28 | ) 29 | .fallbackToDestructiveMigration() 30 | .build() 31 | .also { INSTANCE = it } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 24 | 25 |