├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── jarRepositories.xml ├── misc.xml └── runConfigurations.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── net │ │ └── simplifiedcoding │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── net │ │ │ └── simplifiedcoding │ │ │ ├── MainActivity.kt │ │ │ ├── MyApplication.kt │ │ │ ├── data │ │ │ ├── UserPreferences.kt │ │ │ ├── network │ │ │ │ ├── AuthApi.kt │ │ │ │ ├── BaseApi.kt │ │ │ │ ├── RemoteDataSource.kt │ │ │ │ ├── Resource.kt │ │ │ │ ├── SafeApiCall.kt │ │ │ │ ├── TokenAuthenticator.kt │ │ │ │ ├── TokenRefreshApi.kt │ │ │ │ └── UserApi.kt │ │ │ ├── repository │ │ │ │ ├── AuthRepository.kt │ │ │ │ ├── BaseRepository.kt │ │ │ │ └── UserRepository.kt │ │ │ └── responses │ │ │ │ ├── LoginResponse.kt │ │ │ │ ├── TokenResponse.kt │ │ │ │ └── User.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ └── ui │ │ │ ├── Utils.kt │ │ │ ├── auth │ │ │ ├── AuthActivity.kt │ │ │ ├── AuthViewModel.kt │ │ │ ├── AuthViewModelFactory.kt │ │ │ ├── Factory.kt │ │ │ ├── LoginFragment.kt │ │ │ └── RegisterFragment.kt │ │ │ ├── base │ │ │ └── BaseViewModel.kt │ │ │ └── home │ │ │ ├── HomeActivity.kt │ │ │ ├── HomeFragment.kt │ │ │ └── HomeViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── app_logo.png │ │ ├── bg_button_primary.xml │ │ ├── bg_login_page1.xml │ │ └── ic_launcher_background.xml │ │ ├── font │ │ ├── poppins_bold.ttf │ │ ├── poppins_light.ttf │ │ ├── poppins_regular.ttf │ │ └── poppins_semibold.ttf │ │ ├── layout │ │ ├── activity_auth.xml │ │ ├── activity_home.xml │ │ ├── activity_main.xml │ │ ├── fragment_home.xml │ │ ├── fragment_login.xml │ │ └── fragment_register.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ │ ├── navigation │ │ ├── nav_auth.xml │ │ └── nav_home.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── net │ └── simplifiedcoding │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.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 | 16 | 17 | # Built application files 18 | *.apk 19 | *.aar 20 | *.ap_ 21 | *.aab 22 | 23 | # Files for the ART/Dalvik VM 24 | *.dex 25 | 26 | # Java class files 27 | *.class 28 | 29 | # Generated files 30 | bin/ 31 | gen/ 32 | out/ 33 | # Uncomment the following line in case you need and you don't have the release build type files in your app 34 | # release/ 35 | 36 | # Gradle files 37 | .gradle/ 38 | build/ 39 | 40 | # Local configuration file (sdk path, etc) 41 | local.properties 42 | 43 | # Proguard folder generated by Eclipse 44 | proguard/ 45 | 46 | # Log Files 47 | *.log 48 | 49 | # Android Studio Navigation editor temp files 50 | .navigation/ 51 | 52 | # Android Studio captures folder 53 | captures/ 54 | 55 | # IntelliJ 56 | .idea/workspace.xml 57 | .idea/tasks.xml 58 | .idea/gradle.xml 59 | .idea/assetWizardSettings.xml 60 | .idea/dictionaries 61 | .idea/libraries 62 | # Android Studio 3 in .gitignore file. 63 | .idea/caches 64 | .idea/modules.xml 65 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 66 | .idea/navEditor.xml 67 | 68 | # Keystore files 69 | # Uncomment the following lines if you do not want to check your keystore files in. 70 | #*.jks 71 | #*.keystore 72 | 73 | # External native build folder generated in Android Studio 2.2 and later 74 | .cxx/ 75 | 76 | # Google Services (e.g. APIs or Firebase) 77 | # google-services.json 78 | 79 | # Freeline 80 | freeline.py 81 | freeline/ 82 | freeline_project_description.json 83 | 84 | # fastlane 85 | fastlane/report.xml 86 | fastlane/Preview.html 87 | fastlane/screenshots 88 | fastlane/test_output 89 | fastlane/readme.md 90 | 91 | # Version control 92 | vcs.xml 93 | 94 | # lint 95 | lint/intermediates/ 96 | lint/generated/ 97 | lint/outputs/ 98 | lint/tmp/ 99 | # lint/reports/ 100 | 101 | # Android Profiling 102 | *.hprof -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Login Sample -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "kotlin-kapt" 5 | id "androidx.navigation.safeargs" 6 | id "dagger.hilt.android.plugin" 7 | } 8 | 9 | android { 10 | compileSdkVersion 30 11 | buildToolsVersion "30.0.2" 12 | 13 | defaultConfig { 14 | applicationId "net.simplifiedcoding" 15 | minSdkVersion 16 16 | targetSdkVersion 30 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 27 | } 28 | } 29 | 30 | buildFeatures { 31 | viewBinding true 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | 39 | kotlinOptions { 40 | jvmTarget = JavaVersion.VERSION_1_8.toString() 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(dir: "libs", include: ["*.jar"]) 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 47 | implementation "androidx.core:core-ktx:1.3.2" 48 | implementation "androidx.appcompat:appcompat:1.2.0" 49 | implementation "androidx.constraintlayout:constraintlayout:2.0.4" 50 | implementation "androidx.annotation:annotation:1.2.0" 51 | implementation "androidx.legacy:legacy-support-v4:1.0.0" 52 | testImplementation "junit:junit:4.13.2" 53 | androidTestImplementation "androidx.test.ext:junit:1.1.2" 54 | androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" 55 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" 56 | implementation "com.squareup.retrofit2:retrofit:2.9.0" 57 | implementation "com.squareup.retrofit2:converter-gson:2.9.0" 58 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" 59 | implementation "com.google.android.material:material:1.4.0-alpha02" 60 | implementation "androidx.navigation:navigation-fragment-ktx:2.3.5" 61 | implementation "androidx.navigation:navigation-ui-ktx:2.3.5" 62 | implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" 63 | implementation "androidx.datastore:datastore-preferences:1.0.0-beta01" 64 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" 65 | implementation "androidx.fragment:fragment-ktx:1.3.3" 66 | implementation "com.google.dagger:hilt-android:2.35" 67 | kapt "com.google.dagger:hilt-compiler:2.35" 68 | } -------------------------------------------------------------------------------- /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/androidTest/java/net/simplifiedcoding/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding 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("net.simplifiedcoding", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.lifecycle.Observer 6 | import androidx.lifecycle.asLiveData 7 | import net.simplifiedcoding.data.UserPreferences 8 | import net.simplifiedcoding.ui.auth.AuthActivity 9 | import net.simplifiedcoding.ui.home.HomeActivity 10 | import net.simplifiedcoding.ui.startNewActivity 11 | 12 | 13 | class MainActivity : AppCompatActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.activity_main) 18 | val userPreferences = UserPreferences(this) 19 | 20 | userPreferences.accessToken.asLiveData().observe(this, Observer { 21 | val activity = if (it == null) AuthActivity::class.java else HomeActivity::class.java 22 | startNewActivity(activity) 23 | }) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MyApplication : Application() -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/UserPreferences.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.edit 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.map 12 | import javax.inject.Inject 13 | 14 | private val Context.dataStore: DataStore by preferencesDataStore(name = "my_data_store") 15 | 16 | class UserPreferences @Inject constructor (@ApplicationContext context: Context) { 17 | 18 | private val appContext = context.applicationContext 19 | 20 | val accessToken: Flow 21 | get() = appContext.dataStore.data.map { preferences -> 22 | preferences[ACCESS_TOKEN] 23 | } 24 | 25 | val refreshToken: Flow 26 | get() = appContext.dataStore.data.map { preferences -> 27 | preferences[REFRESH_TOKEN] 28 | } 29 | 30 | suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { 31 | appContext.dataStore.edit { preferences -> 32 | preferences[ACCESS_TOKEN] = accessToken 33 | preferences[REFRESH_TOKEN] = refreshToken 34 | } 35 | } 36 | 37 | suspend fun clear() { 38 | appContext.dataStore.edit { preferences -> 39 | preferences.clear() 40 | } 41 | } 42 | 43 | companion object { 44 | private val ACCESS_TOKEN = stringPreferencesKey("key_access_token") 45 | private val REFRESH_TOKEN = stringPreferencesKey("key_refresh_token") 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/AuthApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import net.simplifiedcoding.data.responses.LoginResponse 4 | import net.simplifiedcoding.data.responses.TokenResponse 5 | import retrofit2.http.Field 6 | import retrofit2.http.FormUrlEncoded 7 | import retrofit2.http.POST 8 | 9 | interface AuthApi : BaseApi { 10 | 11 | @FormUrlEncoded 12 | @POST("auth/login") 13 | suspend fun login( 14 | @Field("email") email: String, 15 | @Field("password") password: String 16 | ): LoginResponse 17 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/BaseApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import okhttp3.ResponseBody 4 | import retrofit2.http.POST 5 | 6 | interface BaseApi { 7 | @POST("logout") 8 | suspend fun logout(): ResponseBody 9 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/RemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import android.content.Context 4 | import net.simplifiedcoding.BuildConfig 5 | import okhttp3.Authenticator 6 | import okhttp3.OkHttpClient 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | import javax.inject.Inject 11 | 12 | 13 | class RemoteDataSource @Inject constructor() { 14 | 15 | companion object { 16 | private const val BASE_URL = "http://apix.simplifiedcoding.in/api/" 17 | } 18 | 19 | fun buildApi( 20 | api: Class, 21 | context: Context 22 | ): Api { 23 | val authenticator = TokenAuthenticator(context, buildTokenApi()) 24 | return Retrofit.Builder() 25 | .baseUrl(BASE_URL) 26 | .client(getRetrofitClient(authenticator)) 27 | .addConverterFactory(GsonConverterFactory.create()) 28 | .build() 29 | .create(api) 30 | } 31 | 32 | private fun buildTokenApi(): TokenRefreshApi { 33 | return Retrofit.Builder() 34 | .baseUrl(BASE_URL) 35 | .client(getRetrofitClient()) 36 | .addConverterFactory(GsonConverterFactory.create()) 37 | .build() 38 | .create(TokenRefreshApi::class.java) 39 | } 40 | 41 | private fun getRetrofitClient(authenticator: Authenticator? = null): OkHttpClient { 42 | return OkHttpClient.Builder() 43 | .addInterceptor { chain -> 44 | chain.proceed(chain.request().newBuilder().also { 45 | it.addHeader("Accept", "application/json") 46 | }.build()) 47 | }.also { client -> 48 | authenticator?.let { client.authenticator(it) } 49 | if (BuildConfig.DEBUG) { 50 | val logging = HttpLoggingInterceptor() 51 | logging.setLevel(HttpLoggingInterceptor.Level.BODY) 52 | client.addInterceptor(logging) 53 | } 54 | }.build() 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/Resource.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import okhttp3.ResponseBody 4 | 5 | sealed class Resource { 6 | data class Success(val value: T) : Resource() 7 | data class Failure( 8 | val isNetworkError: Boolean, 9 | val errorCode: Int?, 10 | val errorBody: ResponseBody? 11 | ) : Resource() 12 | object Loading : Resource() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/SafeApiCall.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import retrofit2.HttpException 6 | 7 | interface SafeApiCall { 8 | suspend fun safeApiCall( 9 | apiCall: suspend () -> T 10 | ): Resource { 11 | return withContext(Dispatchers.IO) { 12 | try { 13 | Resource.Success(apiCall.invoke()) 14 | } catch (throwable: Throwable) { 15 | when (throwable) { 16 | is HttpException -> { 17 | Resource.Failure(false, throwable.code(), throwable.response()?.errorBody()) 18 | } 19 | else -> { 20 | Resource.Failure(true, null, null) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/TokenAuthenticator.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import android.content.Context 4 | import kotlinx.coroutines.flow.first 5 | import kotlinx.coroutines.runBlocking 6 | import net.simplifiedcoding.data.UserPreferences 7 | import net.simplifiedcoding.data.repository.BaseRepository 8 | import net.simplifiedcoding.data.responses.TokenResponse 9 | import okhttp3.Authenticator 10 | import okhttp3.Request 11 | import okhttp3.Response 12 | import okhttp3.Route 13 | 14 | class TokenAuthenticator( 15 | context: Context, 16 | private val tokenApi: TokenRefreshApi 17 | ) : Authenticator, BaseRepository(tokenApi) { 18 | 19 | private val appContext = context.applicationContext 20 | private val userPreferences = UserPreferences(appContext) 21 | 22 | override fun authenticate(route: Route?, response: Response): Request? { 23 | return runBlocking { 24 | when (val tokenResponse = getUpdatedToken()) { 25 | is Resource.Success -> { 26 | userPreferences.saveAccessTokens( 27 | tokenResponse.value.access_token!!, 28 | tokenResponse.value.refresh_token!! 29 | ) 30 | response.request.newBuilder() 31 | .header("Authorization", "Bearer ${tokenResponse.value.access_token}") 32 | .build() 33 | } 34 | else -> null 35 | } 36 | } 37 | } 38 | 39 | private suspend fun getUpdatedToken(): Resource { 40 | val refreshToken = userPreferences.refreshToken.first() 41 | return safeApiCall { tokenApi.refreshAccessToken(refreshToken) } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/TokenRefreshApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import net.simplifiedcoding.data.responses.TokenResponse 4 | import retrofit2.http.Field 5 | import retrofit2.http.FormUrlEncoded 6 | import retrofit2.http.POST 7 | 8 | interface TokenRefreshApi : BaseApi { 9 | @FormUrlEncoded 10 | @POST("auth/refresh-token") 11 | suspend fun refreshAccessToken( 12 | @Field("refresh_token") refreshToken: String? 13 | ): TokenResponse 14 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/network/UserApi.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.network 2 | 3 | import net.simplifiedcoding.data.responses.LoginResponse 4 | import okhttp3.ResponseBody 5 | import retrofit2.http.GET 6 | import retrofit2.http.POST 7 | 8 | interface UserApi : BaseApi{ 9 | @GET("user") 10 | suspend fun getUser(): LoginResponse 11 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/repository/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.repository 2 | 3 | import net.simplifiedcoding.data.UserPreferences 4 | import net.simplifiedcoding.data.network.AuthApi 5 | import javax.inject.Inject 6 | 7 | 8 | class AuthRepository @Inject constructor ( 9 | private val api: AuthApi, 10 | private val preferences: UserPreferences 11 | ) : BaseRepository(api) { 12 | 13 | suspend fun login( 14 | email: String, 15 | password: String 16 | ) = safeApiCall { 17 | api.login(email, password) 18 | } 19 | 20 | suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { 21 | preferences.saveAccessTokens(accessToken, refreshToken) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/repository/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.repository 2 | 3 | import net.simplifiedcoding.data.network.BaseApi 4 | import net.simplifiedcoding.data.network.SafeApiCall 5 | 6 | abstract class BaseRepository(private val api: BaseApi) : SafeApiCall { 7 | 8 | suspend fun logout() = safeApiCall { 9 | api.logout() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.repository 2 | 3 | import net.simplifiedcoding.data.network.UserApi 4 | 5 | class UserRepository( 6 | private val api: UserApi 7 | ) : BaseRepository(api) { 8 | 9 | suspend fun getUser() = safeApiCall { api.getUser() } 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/responses/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.responses 2 | 3 | data class LoginResponse( 4 | val user: User 5 | ) -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/responses/TokenResponse.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.responses 2 | 3 | data class TokenResponse( 4 | val access_token: String?, 5 | val refresh_token: String? 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/data/responses/User.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.data.responses 2 | 3 | data class User( 4 | val access_token: String?, 5 | val refresh_token: String?, 6 | val created_at: String, 7 | val email: String, 8 | val email_verified_at: Any, 9 | val id: Int, 10 | val name: String, 11 | val updated_at: String 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import net.simplifiedcoding.data.network.AuthApi 10 | import net.simplifiedcoding.data.network.RemoteDataSource 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object AppModule { 15 | 16 | @Provides 17 | fun provideAuthApi( 18 | @ApplicationContext context: Context, 19 | remoteDataSource: RemoteDataSource 20 | ): AuthApi { 21 | return remoteDataSource.buildApi(AuthApi::class.java, context) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/Utils.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import androidx.lifecycle.lifecycleScope 8 | import com.google.android.material.snackbar.Snackbar 9 | import kotlinx.coroutines.launch 10 | import net.simplifiedcoding.data.network.Resource 11 | import net.simplifiedcoding.ui.auth.LoginFragment 12 | import net.simplifiedcoding.ui.home.HomeActivity 13 | 14 | fun Activity.startNewActivity(activity: Class) { 15 | Intent(this, activity).also { 16 | it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 17 | startActivity(it) 18 | } 19 | } 20 | 21 | fun View.visible(isVisible: Boolean) { 22 | visibility = if (isVisible) View.VISIBLE else View.GONE 23 | } 24 | 25 | fun View.enable(enabled: Boolean) { 26 | isEnabled = enabled 27 | alpha = if (enabled) 1f else 0.5f 28 | } 29 | 30 | fun View.snackbar(message: String, action: (() -> Unit)? = null) { 31 | val snackbar = Snackbar.make(this, message, Snackbar.LENGTH_LONG) 32 | action?.let { 33 | snackbar.setAction("Retry") { 34 | it() 35 | } 36 | } 37 | snackbar.show() 38 | } 39 | 40 | fun Fragment.logout() = lifecycleScope.launch { 41 | if (activity is HomeActivity) { 42 | (activity as HomeActivity).performLogout() 43 | } 44 | } 45 | 46 | fun Fragment.handleApiError( 47 | failure: Resource.Failure, 48 | retry: (() -> Unit)? = null 49 | ) { 50 | when { 51 | failure.isNetworkError -> requireView().snackbar( 52 | "Please check your internet connection", 53 | retry 54 | ) 55 | failure.errorCode == 401 -> { 56 | if (this is LoginFragment) { 57 | requireView().snackbar("You've entered incorrect email or password") 58 | } else { 59 | logout() 60 | } 61 | } 62 | else -> { 63 | val error = failure.errorBody?.string().toString() 64 | requireView().snackbar(error) 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/AuthActivity.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import dagger.hilt.android.AndroidEntryPoint 6 | import net.simplifiedcoding.R 7 | 8 | @AndroidEntryPoint 9 | class AuthActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_auth) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import kotlinx.coroutines.launch 8 | import net.simplifiedcoding.data.network.Resource 9 | import net.simplifiedcoding.data.repository.AuthRepository 10 | import net.simplifiedcoding.data.responses.LoginResponse 11 | import net.simplifiedcoding.ui.base.BaseViewModel 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class AuthViewModel @Inject constructor( 16 | private val repository: AuthRepository 17 | ) : BaseViewModel(repository) { 18 | 19 | private val _loginResponse: MutableLiveData> = MutableLiveData() 20 | val loginResponse: LiveData> 21 | get() = _loginResponse 22 | 23 | fun login( 24 | email: String, 25 | password: String 26 | ) = viewModelScope.launch { 27 | _loginResponse.value = Resource.Loading 28 | _loginResponse.value = repository.login(email, password) 29 | } 30 | 31 | suspend fun saveAccessTokens(accessToken: String, refreshToken: String) { 32 | repository.saveAccessTokens(accessToken, refreshToken) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/AuthViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import net.simplifiedcoding.data.repository.AuthRepository 4 | 5 | class AuthViewModelFactory( 6 | private val authRepository: AuthRepository 7 | ) : Factory { 8 | override fun create(): AuthViewModel { 9 | return AuthViewModel(authRepository) 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/Factory.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | interface Factory { 4 | fun create() : T 5 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.core.widget.addTextChangedListener 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.viewModels 8 | import androidx.lifecycle.lifecycleScope 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import kotlinx.coroutines.launch 11 | import net.simplifiedcoding.R 12 | import net.simplifiedcoding.data.network.RemoteDataSource 13 | import net.simplifiedcoding.data.network.Resource 14 | import net.simplifiedcoding.databinding.FragmentLoginBinding 15 | import net.simplifiedcoding.ui.enable 16 | import net.simplifiedcoding.ui.handleApiError 17 | import net.simplifiedcoding.ui.home.HomeActivity 18 | import net.simplifiedcoding.ui.startNewActivity 19 | import net.simplifiedcoding.ui.visible 20 | import javax.inject.Inject 21 | 22 | @AndroidEntryPoint 23 | class LoginFragment : Fragment(R.layout.fragment_login) { 24 | 25 | private lateinit var binding: FragmentLoginBinding 26 | private val viewModel: AuthViewModel by viewModels() 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | binding = FragmentLoginBinding.bind(view) 31 | 32 | binding.progressbar.visible(false) 33 | binding.buttonLogin.enable(false) 34 | 35 | viewModel.loginResponse?.observe(viewLifecycleOwner) { 36 | binding.progressbar.visible(it is Resource.Loading) 37 | when (it) { 38 | is Resource.Success -> { 39 | lifecycleScope.launch { 40 | viewModel.saveAccessTokens( 41 | it.value.user.access_token!!, 42 | it.value.user.refresh_token!! 43 | ) 44 | requireActivity().startNewActivity(HomeActivity::class.java) 45 | } 46 | } 47 | is Resource.Failure -> handleApiError(it) { login() } 48 | } 49 | } 50 | 51 | binding.editTextTextPassword.addTextChangedListener { 52 | val email = binding.editTextTextEmailAddress.text.toString().trim() 53 | binding.buttonLogin.enable(email.isNotEmpty() && it.toString().isNotEmpty()) 54 | } 55 | 56 | binding.buttonLogin.setOnClickListener { 57 | login() 58 | } 59 | } 60 | 61 | private fun login() { 62 | val email = binding.editTextTextEmailAddress.text.toString().trim() 63 | val password = binding.editTextTextPassword.text.toString().trim() 64 | viewModel.login(email, password) 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/auth/RegisterFragment.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.auth 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import net.simplifiedcoding.R 9 | 10 | class RegisterFragment : Fragment() { 11 | 12 | override fun onCreateView( 13 | inflater: LayoutInflater, container: ViewGroup?, 14 | savedInstanceState: Bundle? 15 | ): View? { 16 | return inflater.inflate(R.layout.fragment_register, container, false) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import net.simplifiedcoding.data.network.UserApi 7 | import net.simplifiedcoding.data.repository.BaseRepository 8 | 9 | abstract class BaseViewModel( 10 | private val repository: BaseRepository 11 | ) : ViewModel() { 12 | suspend fun logout() = withContext(Dispatchers.IO) { repository.logout() } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.home 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.lifecycle.lifecycleScope 6 | import kotlinx.coroutines.launch 7 | import net.simplifiedcoding.R 8 | import net.simplifiedcoding.data.UserPreferences 9 | import net.simplifiedcoding.data.network.RemoteDataSource 10 | import net.simplifiedcoding.data.network.UserApi 11 | import net.simplifiedcoding.data.repository.UserRepository 12 | import net.simplifiedcoding.ui.auth.AuthActivity 13 | import net.simplifiedcoding.ui.startNewActivity 14 | 15 | class HomeActivity : AppCompatActivity() { 16 | 17 | lateinit var userPreferences: UserPreferences 18 | lateinit var viewModel: HomeViewModel 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_home) 23 | 24 | userPreferences = UserPreferences(this) 25 | val remoteDataSource = RemoteDataSource() 26 | val api = remoteDataSource.buildApi(UserApi::class.java,this) 27 | val authRepository = UserRepository(api) 28 | viewModel = HomeViewModel(authRepository) 29 | } 30 | 31 | fun performLogout() = lifecycleScope.launch { 32 | viewModel.logout() 33 | userPreferences.clear() 34 | startNewActivity(AuthActivity::class.java) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.home 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import net.simplifiedcoding.R 7 | import net.simplifiedcoding.data.network.RemoteDataSource 8 | import net.simplifiedcoding.data.network.Resource 9 | import net.simplifiedcoding.data.network.UserApi 10 | import net.simplifiedcoding.data.repository.UserRepository 11 | import net.simplifiedcoding.data.responses.User 12 | import net.simplifiedcoding.databinding.FragmentHomeBinding 13 | import net.simplifiedcoding.ui.handleApiError 14 | import net.simplifiedcoding.ui.logout 15 | import net.simplifiedcoding.ui.visible 16 | 17 | class HomeFragment : Fragment(R.layout.fragment_home) { 18 | 19 | private lateinit var binding: FragmentHomeBinding 20 | private lateinit var viewModel: HomeViewModel 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | binding = FragmentHomeBinding.bind(view) 25 | 26 | val remoteDataSource = RemoteDataSource() 27 | val api = remoteDataSource.buildApi(UserApi::class.java, requireContext()) 28 | val userRepository = UserRepository(api) 29 | viewModel = HomeViewModel(userRepository) 30 | 31 | binding.progressbar.visible(false) 32 | viewModel.getUser() 33 | viewModel.user.observe(viewLifecycleOwner){ 34 | when (it) { 35 | is Resource.Success -> { 36 | binding.progressbar.visible(false) 37 | updateUI(it.value.user) 38 | } 39 | is Resource.Loading -> { 40 | binding.progressbar.visible(true) 41 | } 42 | is Resource.Failure -> { 43 | handleApiError(it) 44 | } 45 | } 46 | } 47 | 48 | binding.buttonLogout.setOnClickListener { 49 | logout() 50 | } 51 | } 52 | 53 | private fun updateUI(user: User) { 54 | with(binding) { 55 | textViewId.text = user.id.toString() 56 | textViewName.text = user.name 57 | textViewEmail.text = user.email 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/net/simplifiedcoding/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.simplifiedcoding.ui.home 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.launch 7 | import net.simplifiedcoding.data.network.Resource 8 | import net.simplifiedcoding.data.repository.UserRepository 9 | import net.simplifiedcoding.data.responses.LoginResponse 10 | import net.simplifiedcoding.ui.base.BaseViewModel 11 | 12 | class HomeViewModel( 13 | private val repository: UserRepository 14 | ) : BaseViewModel(repository) { 15 | 16 | private val _user: MutableLiveData> = MutableLiveData() 17 | val user: LiveData> 18 | get() = _user 19 | 20 | fun getUser() = viewModelScope.launch { 21 | _user.value = Resource.Loading 22 | _user.value = repository.getUser() 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/app_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/android-dependency-injection/d513264a41261cdbbd1399b20d2bed5c3ccde65f/app/src/main/res/drawable/app_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_login_page1.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/android-dependency-injection/d513264a41261cdbbd1399b20d2bed5c3ccde65f/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/android-dependency-injection/d513264a41261cdbbd1399b20d2bed5c3ccde65f/app/src/main/res/font/poppins_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/android-dependency-injection/d513264a41261cdbbd1399b20d2bed5c3ccde65f/app/src/main/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probelalkhan/android-dependency-injection/d513264a41261cdbbd1399b20d2bed5c3ccde65f/app/src/main/res/font/poppins_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_auth.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 | 18 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 |