From b608d0442bef94f65d7941d1469dadcf4308dad7 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 24 Feb 2021 13:52:11 +0200 Subject: [PATCH 01/28] Added Room library and extracted the libraries version inside build.gradle(project level) --- app/build.gradle | 41 +++++++++++++++++++++++------------------ build.gradle | 18 +++++++++++++++++- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e827a0..eb044d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,41 +40,46 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.3.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation "androidx.core:core-ktx:$core_ktx" + implementation "androidx.appcompat:appcompat:$appcompat" + implementation "com.google.android.material:material:$material" + implementation "androidx.constraintlayout:constraintlayout:$constraint_layout" testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation "androidx.test.ext:junit:$junit" + androidTestImplementation "androidx.test.espresso:espresso-core:$espresso" // Navigation implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" // Koin - implementation "org.koin:koin-android:2.0.1" - implementation 'org.koin:koin-androidx-viewmodel:2.0.1' - implementation 'org.koin:koin-androidx-scope:2.0.1' + implementation "org.koin:koin-android:$koin" + implementation "org.koin:koin-androidx-viewmodel:$koin" + implementation "org.koin:koin-androidx-scope:$koin" //Retrofit - implementation 'com.squareup.retrofit2:retrofit:2.6.1' - implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' + implementation "com.squareup.retrofit2:retrofit:$retrofit" + implementation "com.squareup.okhttp3:logging-interceptor:$okhttp" // Gson - implementation 'com.squareup.retrofit2:converter-gson:2.6.1' + implementation "com.squareup.retrofit2:converter-gson:$gson" //Glide - implementation 'com.github.bumptech.glide:glide:4.11.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' + implementation "com.github.bumptech.glide:glide:$glide" + kapt "com.github.bumptech.glide:compiler:$glide" //Hawk - implementation 'com.orhanobut:hawk:2.0.1' + implementation "com.orhanobut:hawk:$hawk" //Timber - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation "com.jakewharton.timber:timber:$timber" //Calligraphy - implementation 'io.github.inflationx:calligraphy3:3.1.1' - implementation 'io.github.inflationx:viewpump:2.0.3' + implementation "io.github.inflationx:calligraphy3:$calligraphy" + implementation "io.github.inflationx:viewpump:$viewPump" + + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8076f01..838e2d0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,24 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext{ + ext { kotlin_version = "1.4.30" + core_ktx = "1.3.2" + appcompat = "1.2.0" + material = "1.3.0" + constraint_layout = "2.0.4" + junit = "1.1.2" + espresso = "3.3.0" navigationVersion = "2.3.3" + room_version = "2.2.6" + calligraphy = "3.1.1" + viewPump = "2.0.3" + timber = "4.7.1" + hawk = "2.0.1" + glide = "4.11.0" + gson = "2.6.1" + okhttp = "3.11.0" + retrofit = "2.6.1" + koin = "2.0.1" } repositories { From 9d38387fe6241a35c780356f25ad6b606fffa337 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 24 Feb 2021 13:53:57 +0200 Subject: [PATCH 02/28] Implemented a bottom navigation bar with two items (users & favorites) and finish its setup. Created classes for favorite users screen (fragment, module, view model, layout) --- .../main/java/com/base_android_template/App.kt | 3 ++- .../com/base_android_template/MainActivity.kt | 18 ++++++++++++++++++ .../FavoriteGithubUsersFragment.kt | 13 +++++++++++++ .../FavoriteGithubUsersModule.kt | 8 ++++++++ .../FavoriteGithubUsersViewModel.kt | 6 ++++++ app/src/main/res/color/bottom_nav_color.xml | 5 +++++ .../res/drawable/ic_account_circle_24px.xml | 9 +++++++++ app/src/main/res/drawable/ic_favorite_24px.xml | 9 +++++++++ .../res/drawable/ic_favorite_border_24px.xml | 9 +++++++++ app/src/main/res/layout/activity_main.xml | 18 ++++++++++++++++-- .../layout/fragment_favorite_github_users.xml | 9 +++++++++ app/src/main/res/menu/bottom_navigation.xml | 15 +++++++++++++++ app/src/main/res/navigation/main_nav_graph.xml | 15 ++++++++++++--- app/src/main/res/values-ro/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 15 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt create mode 100644 app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt create mode 100644 app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt create mode 100644 app/src/main/res/color/bottom_nav_color.xml create mode 100644 app/src/main/res/drawable/ic_account_circle_24px.xml create mode 100644 app/src/main/res/drawable/ic_favorite_24px.xml create mode 100644 app/src/main/res/drawable/ic_favorite_border_24px.xml create mode 100644 app/src/main/res/layout/fragment_favorite_github_users.xml create mode 100644 app/src/main/res/menu/bottom_navigation.xml diff --git a/app/src/main/java/com/base_android_template/App.kt b/app/src/main/java/com/base_android_template/App.kt index c33e2b2..43d7d1f 100644 --- a/app/src/main/java/com/base_android_template/App.kt +++ b/app/src/main/java/com/base_android_template/App.kt @@ -3,6 +3,7 @@ package com.base_android_template import android.app.Application import android.content.res.Configuration import com.base_android_template.di.createCoreModules +import com.base_android_template.feature.favorite_users.favoriteGithubUsersModule import com.base_android_template.feature.github_users.githubUsersModule import com.base_android_template.shared.Locales import com.base_android_template.utils.language.LocaleUtils @@ -42,7 +43,7 @@ class App : Application() { } private fun getAppModules() = createCoreModules() + - githubUsersModule + githubUsersModule + favoriteGithubUsersModule private fun initCalligraphy() { ViewPump.init( diff --git a/app/src/main/java/com/base_android_template/MainActivity.kt b/app/src/main/java/com/base_android_template/MainActivity.kt index 964202b..3f3cec9 100644 --- a/app/src/main/java/com/base_android_template/MainActivity.kt +++ b/app/src/main/java/com/base_android_template/MainActivity.kt @@ -4,8 +4,11 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.NavigationUI import com.base_android_template.shared.Locales import com.base_android_template.utils.language.LocaleUtils +import com.google.android.material.bottomnavigation.BottomNavigationView import java.util.Locale @@ -18,6 +21,8 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + setUpNavigation() } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -40,4 +45,17 @@ class MainActivity : AppCompatActivity() { else -> super.onOptionsItemSelected(item) } } + + private fun setUpNavigation() { + val bottomNavigationView = findViewById(R.id.bottom_navigation) + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.main_nav_host_fragment) as NavHostFragment? + navHostFragment?.navController?.let { + NavigationUI.setupWithNavController( + bottomNavigationView, + it + ) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt new file mode 100644 index 0000000..05ad4ea --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt @@ -0,0 +1,13 @@ +package com.base_android_template.feature.favorite_users + +import com.base_android_template.R +import com.base_android_template.base.BaseFragment +import com.base_android_template.databinding.FragmentFavoriteGithubUsersBinding +import org.koin.androidx.viewmodel.ext.android.viewModel + +class FavoriteGithubUsersFragment : + BaseFragment(R.layout.fragment_favorite_github_users) { + + override val viewModel: FavoriteGithubUsersViewModel by viewModel() + +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt new file mode 100644 index 0000000..aad60d9 --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt @@ -0,0 +1,8 @@ +package com.base_android_template.feature.favorite_users + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val favoriteGithubUsersModule = module { + viewModel { FavoriteGithubUsersViewModel() } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt new file mode 100644 index 0000000..847035f --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt @@ -0,0 +1,6 @@ +package com.base_android_template.feature.favorite_users + +import com.base_android_template.base.BaseViewModel + +class FavoriteGithubUsersViewModel : BaseViewModel() { +} \ No newline at end of file diff --git a/app/src/main/res/color/bottom_nav_color.xml b/app/src/main/res/color/bottom_nav_color.xml new file mode 100644 index 0000000..7eb7368 --- /dev/null +++ b/app/src/main/res/color/bottom_nav_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_account_circle_24px.xml b/app/src/main/res/drawable/ic_account_circle_24px.xml new file mode 100644 index 0000000..b845dc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_account_circle_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_24px.xml b/app/src/main/res/drawable/ic_favorite_24px.xml new file mode 100644 index 0000000..ce351f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_border_24px.xml b/app/src/main/res/drawable/ic_favorite_border_24px.xml new file mode 100644 index 0000000..e664670 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_border_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fdd0605..095d085 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -21,11 +21,25 @@ android:layout_width="@dimen/match_constraint" android:layout_height="@dimen/match_constraint" app:defaultNavHost="true" + app:layout_constraintBottom_toTopOf="@+id/bottom_navigation" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/main_nav_graph" /> + + + app:menu="@menu/bottom_navigation" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favorite_github_users.xml b/app/src/main/res/layout/fragment_favorite_github_users.xml new file mode 100644 index 0000000..78e83e1 --- /dev/null +++ b/app/src/main/res/layout/fragment_favorite_github_users.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_navigation.xml b/app/src/main/res/menu/bottom_navigation.xml new file mode 100644 index 0000000..b43f316 --- /dev/null +++ b/app/src/main/res/menu/bottom_navigation.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 8cb6ffd..f3ef9b7 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -3,12 +3,21 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" - app:startDestination="@id/githubUsersFragment"> + app:startDestination="@id/githubUsers"> + tools:layout="@layout/fragment_github_users" > + + + + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 81b52b5..28b7cfa 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2,5 +2,7 @@ Romana Engleza + Useri + Favoriti Lista cu toti utilizatorii Github, in ordinea in care s-au inregistrat pe GitHub. Aceasta lista include conturi personale de utilizator si conturi de organizatie. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73143c4..30af72c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,5 +4,7 @@ Romanian English + Users + Favorites Lists all users, in the order that they signed up on GitHub. This list includes personal user accounts and organization accounts. \ No newline at end of file From 9058925049535f45605abe907dd344ac40d55d5b Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 24 Feb 2021 16:40:58 +0200 Subject: [PATCH 03/28] Added favorite icon for each Github User item --- app/src/main/res/layout/item_github_user.xml | 22 ++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/item_github_user.xml b/app/src/main/res/layout/item_github_user.xml index ab62a84..b22ce7c 100644 --- a/app/src/main/res/layout/item_github_user.xml +++ b/app/src/main/res/layout/item_github_user.xml @@ -39,16 +39,26 @@ android:id="@+id/github_user_name" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:text="@{item.login}" - android:padding="@dimen/spacing_sixteen" android:fontFamily="@font/lato_regular" - android:textSize="@dimen/font_size_sixteen" + android:padding="@dimen/spacing_sixteen" + android:text="@{item.login}" android:textColor="@color/black" - tools:text="Github User" + android:textSize="@dimen/font_size_sixteen" + app:layout_constraintBottom_toBottomOf="@+id/github_user_avatar" + app:layout_constraintEnd_toStartOf="@+id/favorite_user_button" app:layout_constraintStart_toEndOf="@id/github_user_avatar" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/github_user_avatar" - app:layout_constraintBottom_toBottomOf="@+id/github_user_avatar"/> + tools:text="Github User" /> + + From aaff5852ca970a1b750f7db18299a061468623dc Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 24 Feb 2021 16:43:16 +0200 Subject: [PATCH 04/28] Created the Entity class for Room database -> Github Users --- .../model/response/GithubUserResponse.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt b/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt index 2c9c9f9..ea60da7 100644 --- a/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt +++ b/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt @@ -1,7 +1,13 @@ package com.base_android_template.model.response +import androidx.room.Entity +import androidx.room.Index import com.google.gson.annotations.SerializedName +@Entity( + primaryKeys = ["id"], + indices = [Index("id")] +) data class GithubUserResponse( @field:SerializedName("login") val login: String, From e5c653a05880c246e73be2976f2813e33bfda166 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Fri, 26 Feb 2021 11:37:58 +0200 Subject: [PATCH 05/28] Implemented GithubUsersDatabase and its corresponding DAO. Fixed the issue where FavoriteGithubUsersViewModel cannot be injected --- .../java/com/base_android_template/di/AppModule.kt | 3 ++- .../base_android_template/di/RepositoryModule.kt | 2 +- .../persistance/dao/GithubUsersDao.kt | 13 +++++++++++++ .../persistance/database/GithubUsersDatabase.kt | 4 ++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt create mode 100644 app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt diff --git a/app/src/main/java/com/base_android_template/di/AppModule.kt b/app/src/main/java/com/base_android_template/di/AppModule.kt index 145cc69..75505f4 100644 --- a/app/src/main/java/com/base_android_template/di/AppModule.kt +++ b/app/src/main/java/com/base_android_template/di/AppModule.kt @@ -1,5 +1,6 @@ package com.base_android_template.di +import com.base_android_template.feature.favorite_users.favoriteGithubUsersModule import com.base_android_template.feature.github_users.githubUsersModule -fun getAppModules() = createCoreModules() + githubUsersModule \ No newline at end of file +fun getAppModules() = createCoreModules() + githubUsersModule + favoriteGithubUsersModule \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/di/RepositoryModule.kt b/app/src/main/java/com/base_android_template/di/RepositoryModule.kt index 25fe265..0916c00 100644 --- a/app/src/main/java/com/base_android_template/di/RepositoryModule.kt +++ b/app/src/main/java/com/base_android_template/di/RepositoryModule.kt @@ -5,5 +5,5 @@ import com.base_android_template.repository.GithubUsersRepositoryImpl import org.koin.dsl.module val repositoryModule = module { - factory { GithubUsersRepositoryImpl(githubUsersApi = get()) } + single { GithubUsersRepositoryImpl(githubUsersApi = get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt b/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt new file mode 100644 index 0000000..8642dcf --- /dev/null +++ b/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt @@ -0,0 +1,13 @@ +package com.base_android_template.persistance.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import com.base_android_template.model.response.GithubUserResponse + +@Dao +interface GithubUsersDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertUsers(usersList: List) +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt b/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt new file mode 100644 index 0000000..b53f468 --- /dev/null +++ b/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt @@ -0,0 +1,4 @@ +package com.base_android_template.persistance.database + +class GithubUsersDatabase { +} \ No newline at end of file From 5818ff37436e0af3cacc3af46efc94515ebde836 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Fri, 26 Feb 2021 11:38:23 +0200 Subject: [PATCH 06/28] Added entities to GihubUsersDatabase --- .../persistance/database/GithubUsersDatabase.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt b/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt index b53f468..8f0b326 100644 --- a/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt +++ b/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt @@ -1,4 +1,12 @@ package com.base_android_template.persistance.database -class GithubUsersDatabase { +import androidx.room.Database +import androidx.room.RoomDatabase +import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.persistance.dao.GithubUsersDao + +@Database(entities = [GithubUserResponse::class], version = 1) +abstract class GithubUsersDatabase : RoomDatabase() { + + abstract fun githubUsersDao(): GithubUsersDao } \ No newline at end of file From 8a04350842ea55e0b8d7618dcbaf2e916df2b636 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Thu, 4 Mar 2021 16:49:04 +0200 Subject: [PATCH 07/28] Implemented new entity for saving the Github users list into local database in order to avoid null fields and created a method to map the GithubUserResponse to GithubUserEntity. Implemented insertGithubUsers(), getGithubUsers() and deleteGithubUsers() methods into GithubUsersListDao. Implemented the "Either", "NetworkHandler" and "ExceptionHandler" classes. Added an interface and its implementation for remote Github users list call. Implemented the DatabaseModule to inject room database related classes. Changed the implementation of GithubUsersListUseCase to fetch the list from server and to save it locally in database if the server returns success. Changed the logic in GithubUsersListViewModel to fetch firstly the local users list and if list is empty to call the server endpoint. Updated the adapter, view holder and diffUtil callback clasess to use GithubUserEntity instead of GIthubUserResponse --- .../api/GithubUsersApi.kt | 11 -- .../com/base_android_template/di/ApiModule.kt | 12 ++- .../base_android_template/di/CoreModules.kt | 18 +++- .../di/DatabaseModule.kt | 14 +++ .../di/RepositoryModule.kt | 2 +- .../base_android_template/di/UseCaseModule.kt | 13 ++- .../github_users/GithubUsersCallback.kt | 12 +-- .../github_users/GithubUsersListAdapter.kt | 4 +- .../github_users/GithubUsersListViewHolder.kt | 6 +- .../github_users/GithubUsersViewModel.kt | 36 +++++-- .../model/entity/GithubUserEntity.kt | 90 ++++++++++++++++ .../model/response/GithubUserResponse.kt | 38 +++---- .../persistance/dao/GithubUsersDao.kt | 13 --- .../persistance/dao/GithubUsersListDao.kt | 20 ++++ .../database/GithubUsersDatabase.kt | 8 +- .../remote/GithubUsersApi.kt | 11 ++ .../remote/GithubUsersListRemote.kt | 11 ++ .../remote/GithubUsersListRemoteImpl.kt | 44 ++++++++ .../repository/GithubUsersRepository.kt | 5 +- .../repository/GithubUsersRepositoryImpl.kt | 10 +- .../shared/network/Exception.kt | 11 ++ .../shared/network/ExceptionHandler.kt | 12 +++ .../shared/network/NetworkHandler.kt | 35 ++++++ .../usecase/GetGithubUsersUseCase.kt | 9 +- .../usecase/GetGithubUsersUseCaseImpl.kt | 38 ++++++- .../com/base_android_template/utils/Either.kt | 101 ++++++++++++++++++ app/src/main/res/layout/item_github_user.xml | 2 +- 27 files changed, 494 insertions(+), 92 deletions(-) delete mode 100644 app/src/main/java/com/base_android_template/api/GithubUsersApi.kt create mode 100644 app/src/main/java/com/base_android_template/di/DatabaseModule.kt create mode 100644 app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt delete mode 100644 app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt create mode 100644 app/src/main/java/com/base_android_template/persistance/dao/GithubUsersListDao.kt create mode 100644 app/src/main/java/com/base_android_template/remote/GithubUsersApi.kt create mode 100644 app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt create mode 100644 app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt create mode 100644 app/src/main/java/com/base_android_template/shared/network/Exception.kt create mode 100644 app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt create mode 100644 app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt create mode 100644 app/src/main/java/com/base_android_template/utils/Either.kt diff --git a/app/src/main/java/com/base_android_template/api/GithubUsersApi.kt b/app/src/main/java/com/base_android_template/api/GithubUsersApi.kt deleted file mode 100644 index ff6b8d4..0000000 --- a/app/src/main/java/com/base_android_template/api/GithubUsersApi.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.base_android_template.api - -import com.base_android_template.model.response.GithubUserResponse -import com.base_android_template.shared.network.ApiResponse -import retrofit2.http.GET - -interface GithubUsersApi { - - @GET("/users") - suspend fun getGithubUsers(): ApiResponse, Error> -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/di/ApiModule.kt b/app/src/main/java/com/base_android_template/di/ApiModule.kt index e192b32..058c6b1 100644 --- a/app/src/main/java/com/base_android_template/di/ApiModule.kt +++ b/app/src/main/java/com/base_android_template/di/ApiModule.kt @@ -1,6 +1,8 @@ package com.base_android_template.di -import com.base_android_template.api.GithubUsersApi +import com.base_android_template.remote.GithubUsersApi +import com.base_android_template.remote.GithubUsersListRemote +import com.base_android_template.remote.GithubUsersListRemoteImpl import org.koin.dsl.module import retrofit2.Retrofit @@ -9,5 +11,13 @@ val apiModule = module { return retrofit.create(GithubUsersApi::class.java) } + single { + GithubUsersListRemoteImpl( + githubUsersListService = get(), + networkHandler = get(), + exceptionHandler = get() + ) + } + single { provideUserApi(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/di/CoreModules.kt b/app/src/main/java/com/base_android_template/di/CoreModules.kt index 311a65e..bb436d7 100644 --- a/app/src/main/java/com/base_android_template/di/CoreModules.kt +++ b/app/src/main/java/com/base_android_template/di/CoreModules.kt @@ -1,9 +1,13 @@ package com.base_android_template.di +import android.content.Context +import android.content.Context.CONNECTIVITY_SERVICE +import android.net.ConnectivityManager import com.base_android_template.shared.BASE_URL +import com.base_android_template.shared.network.ExceptionHandler +import com.base_android_template.shared.network.NetworkHandler import com.base_android_template.shared.provider.PreferencesProvider import com.base_android_template.shared.provider.PreferencesProviderImpl -import com.base_android_template.utils.network.NetworkResponseAdapterFactory import com.google.gson.FieldNamingPolicy import com.google.gson.GsonBuilder import okhttp3.OkHttpClient @@ -27,13 +31,21 @@ val coreModules = module { factory { Retrofit.Builder() .baseUrl(BASE_URL) - .addCallAdapterFactory(NetworkResponseAdapterFactory()) .addConverterFactory(GsonConverterFactory.create(get())) .client(get()) .build() } factory { PreferencesProviderImpl() } + + single { ExceptionHandler() } + + fun provideNetworkHandler(context: Context): NetworkHandler = + NetworkHandler(context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager) + + single { provideNetworkHandler(get()) } + } -fun createCoreModules() = coreModules + apiModule + repositoryModule + useCaseModule \ No newline at end of file +fun createCoreModules() = + coreModules + apiModule + repositoryModule + useCaseModule + databaseModule \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/di/DatabaseModule.kt b/app/src/main/java/com/base_android_template/di/DatabaseModule.kt new file mode 100644 index 0000000..7958701 --- /dev/null +++ b/app/src/main/java/com/base_android_template/di/DatabaseModule.kt @@ -0,0 +1,14 @@ +package com.base_android_template.di + +import android.content.Context +import androidx.room.Room +import com.base_android_template.persistance.database.GithubUsersDatabase +import org.koin.dsl.module + +val databaseModule = module { + fun provideGithubUsersListDatabase(context: Context): GithubUsersDatabase = + Room.databaseBuilder(context, GithubUsersDatabase::class.java, "github_users_database") + .build() + + single { provideGithubUsersListDatabase(get()) } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/di/RepositoryModule.kt b/app/src/main/java/com/base_android_template/di/RepositoryModule.kt index 0916c00..c8cee85 100644 --- a/app/src/main/java/com/base_android_template/di/RepositoryModule.kt +++ b/app/src/main/java/com/base_android_template/di/RepositoryModule.kt @@ -5,5 +5,5 @@ import com.base_android_template.repository.GithubUsersRepositoryImpl import org.koin.dsl.module val repositoryModule = module { - single { GithubUsersRepositoryImpl(githubUsersApi = get()) } + single { GithubUsersRepositoryImpl(githubUsersListRemote = get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/di/UseCaseModule.kt b/app/src/main/java/com/base_android_template/di/UseCaseModule.kt index bbdc795..50fc58b 100644 --- a/app/src/main/java/com/base_android_template/di/UseCaseModule.kt +++ b/app/src/main/java/com/base_android_template/di/UseCaseModule.kt @@ -1,9 +1,20 @@ package com.base_android_template.di +import com.base_android_template.persistance.database.GithubUsersDatabase +import com.base_android_template.repository.GithubUsersRepository import com.base_android_template.usecase.GetGithubUsersUseCase import com.base_android_template.usecase.GetGithubUsersUseCaseImpl import org.koin.dsl.module val useCaseModule = module { - factory { GetGithubUsersUseCaseImpl(githubUsersRepository = get()) } + + fun getGithubUsersUseCase( + githubUsersRepository: GithubUsersRepository, + githubUsersDatabase: GithubUsersDatabase, + ): GetGithubUsersUseCase = GetGithubUsersUseCaseImpl( + githubUsersRepository = githubUsersRepository, + githubUsersListDao = githubUsersDatabase.githubUsersListDao() + ) + + factory { getGithubUsersUseCase(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersCallback.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersCallback.kt index 13653f1..62ee87e 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersCallback.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersCallback.kt @@ -1,17 +1,17 @@ package com.base_android_template.feature.github_users import androidx.recyclerview.widget.DiffUtil -import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.model.entity.GithubUserEntity -class GithubUsersCallback : DiffUtil.ItemCallback() { +class GithubUsersCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: GithubUserResponse, - newItem: GithubUserResponse + oldItem: GithubUserEntity, + newItem: GithubUserEntity ): Boolean = oldItem.id == newItem.id override fun areContentsTheSame( - oldItem: GithubUserResponse, - newItem: GithubUserResponse + oldItem: GithubUserEntity, + newItem: GithubUserEntity ): Boolean = oldItem == newItem } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListAdapter.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListAdapter.kt index 42b92af..c077182 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListAdapter.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListAdapter.kt @@ -4,10 +4,10 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter import com.base_android_template.databinding.ItemGithubUserBinding -import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.model.entity.GithubUserEntity class GithubUsersListAdapter : - ListAdapter(GithubUsersCallback()) { + ListAdapter(GithubUsersCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GithubUsersListViewHolder { val inflater = LayoutInflater.from(parent.context) diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt index d378298..ebc92a9 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt @@ -2,13 +2,13 @@ package com.base_android_template.feature.github_users import androidx.recyclerview.widget.RecyclerView import com.base_android_template.databinding.ItemGithubUserBinding -import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.model.entity.GithubUserEntity class GithubUsersListViewHolder(private val binding: ItemGithubUserBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(githubUserResponse: GithubUserResponse) { - binding.item = githubUserResponse + fun bind(githubUserItem: GithubUserEntity) { + binding.item = githubUserItem binding.executePendingBindings() } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt index f524810..ead0081 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt @@ -14,19 +14,35 @@ class GithubUsersViewModel( val githubUsersListAdapter = GithubUsersListAdapter() init { - getGithubUsers() + getLocalCartItems() } - private fun getGithubUsers() { + private fun getLocalCartItems() { viewModelScope.launch { - getGithubUsersUseCase.getGithubUsers().fold({ - githubUsersListAdapter.submitList(it) - }, { - Timber.d( - GithubUsersViewModel::class.simpleName, - "Error fetching Github users list" - ) - }) + getGithubUsersUseCase.getLocalGithubUsers().fold( + { + getRemoteGithubUsers() + }, + { + githubUsersListAdapter.submitList(it) + } + ) + } + } + + private fun getRemoteGithubUsers() { + viewModelScope.launch { + getGithubUsersUseCase.getRemoteAndSaveLocalGithubUsers().fold( + { + Timber.d( + GithubUsersViewModel::class.simpleName, + "Error fetching Github users list" + ) + }, + { + githubUsersListAdapter.submitList(it) + } + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt b/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt new file mode 100644 index 0000000..a918874 --- /dev/null +++ b/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt @@ -0,0 +1,90 @@ +package com.base_android_template.model.entity + +import androidx.room.Entity +import androidx.room.Index +import com.base_android_template.model.response.GithubUserResponse +import com.google.gson.annotations.SerializedName + +@Entity( + primaryKeys = ["id"], + indices = [Index("id")] +) +data class GithubUserEntity( + @field:SerializedName("login") + val login: String = "", + + @field:SerializedName("id") + val id: Int = 0, + + @field:SerializedName("node_id") + val nodeId: String = "", + + @field:SerializedName("avatar_url") + val avatarUrl: String = "", + + @field:SerializedName("gravatar_id") + val grAvatarId: String = "", + + @field:SerializedName("url") + val url: String = "", + + @field:SerializedName("html_url") + val htmlUrl: String = "", + + @field:SerializedName("followers_url") + val followersUrl: String = "", + + @field:SerializedName("following_url") + val followingUrl: String = "", + + @field:SerializedName("gists_url") + val gistsUrl: String = "", + + @field:SerializedName("starred_url") + val starredUrl: String = "", + + @field:SerializedName("subscriptions_url") + val subscriptionsUrl: String = "", + + @field:SerializedName("organizations_url") + val organizationsUrl: String = "", + + @field:SerializedName("repos_url") + val reposUrl: String = "", + + @field:SerializedName("events_url") + val eventsUrl: String = "", + + @field:SerializedName("received_events_url") + val receivedEventsUrl: String = "", + + @field:SerializedName("type") + val type: String = "", + + @field:SerializedName("site_admin") + val siteAdmin: String = "" +) { + companion object { + fun mapToGithubUserEntity(githubUserResponse: GithubUserResponse) = + GithubUserEntity( + login = githubUserResponse.login.orEmpty(), + id = githubUserResponse.id, + nodeId = githubUserResponse.nodeId.orEmpty(), + avatarUrl = githubUserResponse.avatarUrl.orEmpty(), + grAvatarId = githubUserResponse.grAvatarId.orEmpty(), + url = githubUserResponse.url.orEmpty(), + htmlUrl = githubUserResponse.htmlUrl.orEmpty(), + followersUrl = githubUserResponse.followersUrl.orEmpty(), + followingUrl = githubUserResponse.followingUrl.orEmpty(), + gistsUrl = githubUserResponse.gistsUrl.orEmpty(), + starredUrl = githubUserResponse.starredUrl.orEmpty(), + subscriptionsUrl = githubUserResponse.subscriptionsUrl.orEmpty(), + organizationsUrl = githubUserResponse.organizationsUrl.orEmpty(), + reposUrl = githubUserResponse.reposUrl.orEmpty(), + eventsUrl = githubUserResponse.eventsUrl.orEmpty(), + receivedEventsUrl = githubUserResponse.receivedEventsUrl.orEmpty(), + type = githubUserResponse.type.orEmpty(), + siteAdmin = githubUserResponse.siteAdmin.orEmpty() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt b/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt index eee800e..12aa628 100644 --- a/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt +++ b/app/src/main/java/com/base_android_template/model/response/GithubUserResponse.kt @@ -1,64 +1,58 @@ package com.base_android_template.model.response -import androidx.room.Entity -import androidx.room.Index import com.google.gson.annotations.SerializedName -@Entity( - primaryKeys = ["id"], - indices = [Index("id")] -) data class GithubUserResponse( @field:SerializedName("login") - val login: String, + val login: String?, @field:SerializedName("id") val id: Int, @field:SerializedName("node_id") - val nodeId: String, + val nodeId: String?, @field:SerializedName("avatar_url") - val avatarUrl: String, + val avatarUrl: String?, @field:SerializedName("gravatar_id") - val grAvatarId: String, + val grAvatarId: String?, @field:SerializedName("url") - val url: String, + val url: String?, @field:SerializedName("html_url") - val htmlUrl: String, + val htmlUrl: String?, @field:SerializedName("followers_url") - val followersUrl: String, + val followersUrl: String?, @field:SerializedName("following_url") - val followingUrl: String, + val followingUrl: String?, @field:SerializedName("gists_url") - val gistsUrl: String, + val gistsUrl: String?, @field:SerializedName("starred_url") - val starredUrl: String, + val starredUrl: String?, @field:SerializedName("subscriptions_url") - val subscriptionsUrl: String, + val subscriptionsUrl: String?, @field:SerializedName("organizations_url") - val organizationsUrl: String, + val organizationsUrl: String?, @field:SerializedName("repos_url") - val reposUrl: String, + val reposUrl: String?, @field:SerializedName("events_url") - val eventsUrl: String, + val eventsUrl: String?, @field:SerializedName("received_events_url") - val receivedEventsUrl: String, + val receivedEventsUrl: String?, @field:SerializedName("type") - val type: String, + val type: String?, @field:SerializedName("site_admin") val siteAdmin: String diff --git a/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt b/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt deleted file mode 100644 index 8642dcf..0000000 --- a/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersDao.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.base_android_template.persistance.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import com.base_android_template.model.response.GithubUserResponse - -@Dao -interface GithubUsersDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertUsers(usersList: List) -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersListDao.kt b/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersListDao.kt new file mode 100644 index 0000000..7177986 --- /dev/null +++ b/app/src/main/java/com/base_android_template/persistance/dao/GithubUsersListDao.kt @@ -0,0 +1,20 @@ +package com.base_android_template.persistance.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.base_android_template.model.entity.GithubUserEntity + +@Dao +interface GithubUsersListDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertGithubUsers(usersList: List) + + @Query("SELECT * FROM GithubUserEntity") + suspend fun getGithubUsers(): List? + + @Query("DELETE FROM GithubUserEntity") + suspend fun deleteGithubUsers() +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt b/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt index 8f0b326..40cee2a 100644 --- a/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt +++ b/app/src/main/java/com/base_android_template/persistance/database/GithubUsersDatabase.kt @@ -2,11 +2,11 @@ package com.base_android_template.persistance.database import androidx.room.Database import androidx.room.RoomDatabase -import com.base_android_template.model.response.GithubUserResponse -import com.base_android_template.persistance.dao.GithubUsersDao +import com.base_android_template.model.entity.GithubUserEntity +import com.base_android_template.persistance.dao.GithubUsersListDao -@Database(entities = [GithubUserResponse::class], version = 1) +@Database(entities = [GithubUserEntity::class], version = 1) abstract class GithubUsersDatabase : RoomDatabase() { - abstract fun githubUsersDao(): GithubUsersDao + abstract fun githubUsersListDao(): GithubUsersListDao } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersApi.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersApi.kt new file mode 100644 index 0000000..adca36e --- /dev/null +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersApi.kt @@ -0,0 +1,11 @@ +package com.base_android_template.remote + +import com.base_android_template.model.response.GithubUserResponse +import retrofit2.Response +import retrofit2.http.GET + +interface GithubUsersApi { + + @GET("/users") + suspend fun getGithubUsers(): Response> +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt new file mode 100644 index 0000000..29aac79 --- /dev/null +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt @@ -0,0 +1,11 @@ +package com.base_android_template.remote + +import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.utils.Either +import com.base_android_template.shared.network.Exception + +interface GithubUsersListRemote { + + suspend fun getGithubUsersList(): Either> + +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt new file mode 100644 index 0000000..abdd136 --- /dev/null +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt @@ -0,0 +1,44 @@ +package com.base_android_template.remote + +import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.utils.Either +import com.base_android_template.shared.network.Exception +import com.base_android_template.shared.network.ExceptionHandler +import com.base_android_template.shared.network.NetworkHandler +import retrofit2.Response + +class GithubUsersListRemoteImpl( + private val githubUsersListService: GithubUsersApi, + private val networkHandler: NetworkHandler, + private val exceptionHandler: ExceptionHandler +) : GithubUsersListRemote { + + override suspend fun getGithubUsersList(): Either> = makeRequest { + githubUsersListService.getGithubUsers() + } + + private suspend fun makeRequest(block: suspend () -> Response>): Either> { + if (!networkHandler.hasNetworkConnection()) { + return Either.Failure(Exception.NetworkException) + } + return try { + val response = block.invoke() + handleResponse(response) + } catch (exception: Throwable) { + exceptionHandler.handleGeneralException(exception) + } + } + + private fun handleResponse(response: Response>): Either> { + return if (response.isSuccessful) { + handleSuccess(response) + } else { + Either.Failure(Exception.ServerException) + } + } + + private fun handleSuccess(response: Response>): Either.Success> { + val githubUsersList = response.body() ?: listOf() + return Either.Success(githubUsersList) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt b/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt index 87a288c..64b4842 100644 --- a/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt +++ b/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt @@ -1,9 +1,10 @@ package com.base_android_template.repository import com.base_android_template.model.response.GithubUserResponse -import com.base_android_template.shared.network.ApiResponse +import com.base_android_template.utils.Either +import com.base_android_template.shared.network.Exception interface GithubUsersRepository { - suspend fun getGithubUsers(): ApiResponse, Error> + suspend fun getLocalGithubUsers(): Either> } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt b/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt index ddf8ab6..47e7928 100644 --- a/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt +++ b/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt @@ -1,13 +1,15 @@ package com.base_android_template.repository -import com.base_android_template.api.GithubUsersApi import com.base_android_template.model.response.GithubUserResponse -import com.base_android_template.shared.network.ApiResponse +import com.base_android_template.remote.GithubUsersListRemote +import com.base_android_template.utils.Either +import com.base_android_template.shared.network.Exception class GithubUsersRepositoryImpl( - private val githubUsersApi: GithubUsersApi + private val githubUsersListRemote: GithubUsersListRemote ) : GithubUsersRepository { - override suspend fun getGithubUsers(): ApiResponse, Error> = githubUsersApi.getGithubUsers() + override suspend fun getLocalGithubUsers(): Either> = + githubUsersListRemote.getGithubUsersList() } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/Exception.kt b/app/src/main/java/com/base_android_template/shared/network/Exception.kt new file mode 100644 index 0000000..628e64b --- /dev/null +++ b/app/src/main/java/com/base_android_template/shared/network/Exception.kt @@ -0,0 +1,11 @@ +package com.base_android_template.shared.network + +sealed class Exception { + object NetworkException : Exception() + object ServerException : Exception() + object SaveGithubUsersException : Exception() + object EmptyLocalGithubUsersLisException : Exception() + data class ClientException(val errorMessage: String) : Exception() + data class UnknownException(val exception: Throwable) : Exception() + object DataBaseException : Exception() +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt new file mode 100644 index 0000000..0365a05 --- /dev/null +++ b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt @@ -0,0 +1,12 @@ +package com.base_android_template.shared.network + +import com.base_android_template.utils.Either +import java.net.UnknownHostException + +class ExceptionHandler { + + fun handleGeneralException(exception: Throwable): Either.Failure = when (exception) { + is UnknownHostException -> Either.Failure(Exception.NetworkException) + else -> Either.Failure(Exception.UnknownException(exception)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt b/app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt new file mode 100644 index 0000000..1fc0c12 --- /dev/null +++ b/app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt @@ -0,0 +1,35 @@ +package com.base_android_template.shared.network + +import android.Manifest.permission.ACCESS_NETWORK_STATE +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.net.NetworkInfo +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission + +class NetworkHandler constructor(private val connectivityManager: ConnectivityManager) { + + @RequiresPermission(ACCESS_NETWORK_STATE) + fun hasNetworkConnection(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkConnection = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + hasNetworkConnection(networkConnection) + } else { + val networkInfo = connectivityManager.activeNetworkInfo + hasNetworkConnection(networkInfo) + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private fun hasNetworkConnection(networkCapabilities: NetworkCapabilities?): Boolean { + return networkCapabilities != null && (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || networkCapabilities.hasTransport( + NetworkCapabilities.TRANSPORT_CELLULAR + )) + } + + private fun hasNetworkConnection(networkInfo: NetworkInfo?): Boolean { + return networkInfo != null && networkInfo.isConnected + } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt index 17fc1bd..35ddc2c 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt @@ -1,9 +1,12 @@ package com.base_android_template.usecase -import com.base_android_template.model.response.GithubUserResponse -import com.base_android_template.shared.network.ApiResponse +import com.base_android_template.model.entity.GithubUserEntity +import com.base_android_template.utils.Either +import com.base_android_template.shared.network.Exception interface GetGithubUsersUseCase { - suspend fun getGithubUsers(): ApiResponse, Error> + suspend fun getRemoteAndSaveLocalGithubUsers(): Either> + + suspend fun getLocalGithubUsers(): Either> } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt index 41b392f..9ff5e3d 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt @@ -1,13 +1,41 @@ package com.base_android_template.usecase -import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.model.entity.GithubUserEntity +import com.base_android_template.persistance.dao.GithubUsersListDao import com.base_android_template.repository.GithubUsersRepository -import com.base_android_template.shared.network.ApiResponse +import com.base_android_template.utils.Either +import com.base_android_template.utils.doOnSuccess +import com.base_android_template.utils.map +import com.base_android_template.shared.network.Exception +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch class GetGithubUsersUseCaseImpl internal constructor( - private val githubUsersRepository: GithubUsersRepository + private val githubUsersRepository: GithubUsersRepository, + private val githubUsersListDao: GithubUsersListDao ) : GetGithubUsersUseCase { - override suspend fun getGithubUsers(): ApiResponse, Error> = - githubUsersRepository.getGithubUsers() + override suspend fun getRemoteAndSaveLocalGithubUsers(): Either> = + coroutineScope { + githubUsersRepository.getLocalGithubUsers() + .map { githubUsersList -> + githubUsersList.map { GithubUserEntity.mapToGithubUserEntity(it) } + } + .doOnSuccess { + launch { updateLocalGithubUsersList(it) } + } + } + + override suspend fun getLocalGithubUsers(): Either> { + val list = githubUsersListDao.getGithubUsers() + + return if (list?.isNullOrEmpty() == true) + Either.Failure(Exception.EmptyLocalGithubUsersLisException) + else Either.Success(list) + } + + private suspend fun updateLocalGithubUsersList(list: List) { + githubUsersListDao.insertGithubUsers(list) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/utils/Either.kt b/app/src/main/java/com/base_android_template/utils/Either.kt new file mode 100644 index 0000000..b474071 --- /dev/null +++ b/app/src/main/java/com/base_android_template/utils/Either.kt @@ -0,0 +1,101 @@ +package com.base_android_template.utils + +/** + * Represents a value of one of two possible types (a disjoint union). + * Instances of [Either] are either an instance of [Failure] or [Success]. + * FP Convention dictates that [Failure] is used for "failure" + * and [Success] is used for "success". + * + * @see Failure + * @see Success + * @see Credits to Fernando Cejas. + */ +sealed class Either { + /** * Represents the left side of [Either] class which by convention is a "Exception". */ + data class Failure(val error: L) : Either() + + /** * Represents the right side of [Either] class which by convention is a "Success". */ + data class Success(val data: R) : Either() + + /** + * Returns true if this is a Success, false otherwise. + * @see Success + */ + val isSuccess get() = this is Success + + /** + * Returns true if this is a Failure, false otherwise. + * @see Failure + */ + val isFailure get() = this is Failure + + /** + * Creates a Failure type. + * @see Failure + */ + fun failure(a: L) = Failure(a) + + /** + * Creates a Success type. + * @see Success + */ + fun success(b: R) = Success(b) + + /** + * Applies funFailure if this is a Failure or funSuccess if this is a Success. + * @see Failure + * @see Success + */ + fun fold(funFailure: (L) -> Any, funSuccess: (R) -> Any): Any = + when (this) { + is Failure -> funFailure(error) + is Success -> funSuccess(data) + } +} + +/** + * Composes 2 functions + * See Credits to Alex Hart. + */ +fun ((A) -> B).compose(function: (B) -> C): (A) -> C = { + function(this(it)) +} + +/** + * Success-biased flatMap() FP convention which means that Success is assumed to be the default case + * to operate on. If it is Failure, operations like map, flatMap, ... return the Failure value unchanged. + */ +fun Either.flatMap(function: (R) -> Either): Either = + when (this) { + is Either.Failure -> Either.Failure(error) + is Either.Success -> function(data) + } + +/** + * Success-biased map() FP convention which means that Success is assumed to be the default case + * to operate on. If it is Failure, operations like map, flatMap, ... return the Failure value unchanged. + */ +fun Either.map(function: (R) -> (T)): Either = + this.flatMap(function.compose(::success)) + +/** Returns the value from this `Success` or the given argument if this is a `Failure`. + * Right(12).getOrElse(17) RETURNS 12 and Left(12).getOrElse(17) RETURNS 17. + * Just for unit tests + */ +fun Either.getOrElse(value: R): R = + when (this) { + is Either.Failure -> value + is Either.Success -> data + } + +/** Invokes function if Either is success. + * Returns itself. + */ +fun Either.doOnSuccess(block: (R) -> Any): Either = + when (this) { + is Either.Failure -> Either.Failure(error) + is Either.Success -> { + block(data) + Either.Success(data) + } + } \ No newline at end of file diff --git a/app/src/main/res/layout/item_github_user.xml b/app/src/main/res/layout/item_github_user.xml index 7975df3..8f4fa69 100644 --- a/app/src/main/res/layout/item_github_user.xml +++ b/app/src/main/res/layout/item_github_user.xml @@ -6,7 +6,7 @@ + type="com.base_android_template.model.entity.GithubUserEntity" /> Date: Thu, 4 Mar 2021 16:49:26 +0200 Subject: [PATCH 08/28] Removed unused ApiResponse, Error, NetworkResponseAdapter, NetworkResponseCall and NetworkResponseAdapterFactory classes. --- .../shared/network/ApiResponse.kt | 36 ------- .../shared/network/Error.kt | 12 --- .../utils/network/NetworkResponseAdapter.kt | 23 ----- .../network/NetworkResponseAdapterFactory.kt | 49 ---------- .../utils/network/NetworkResponseCall.kt | 93 ------------------- 5 files changed, 213 deletions(-) delete mode 100644 app/src/main/java/com/base_android_template/shared/network/ApiResponse.kt delete mode 100644 app/src/main/java/com/base_android_template/shared/network/Error.kt delete mode 100644 app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapter.kt delete mode 100644 app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapterFactory.kt delete mode 100644 app/src/main/java/com/base_android_template/utils/network/NetworkResponseCall.kt diff --git a/app/src/main/java/com/base_android_template/shared/network/ApiResponse.kt b/app/src/main/java/com/base_android_template/shared/network/ApiResponse.kt deleted file mode 100644 index 01285b4..0000000 --- a/app/src/main/java/com/base_android_template/shared/network/ApiResponse.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.base_android_template.shared.network - -import java.io.IOException - -sealed class ApiResponse { - /** - * Success response with body - */ - data class Success(val body: T?) : ApiResponse() - - /** - * Failure response with body - */ - data class ServerError(val body: U, val code: Int) : ApiResponse() - - /** - * Network error - */ - data class NetworkError(val error: IOException) : ApiResponse() - - /** - * For example, json parsing error - */ - data class UnknownError(val error: Throwable?) : ApiResponse() - - fun fold( - funSuccess: (T?) -> Any, - funFailure: (Error) -> Any - ): Any = - when (this) { - is Success -> funSuccess(body) - is ServerError -> funFailure(Error.ServerError(body)) - is NetworkError -> funFailure(Error.NetworkError(error)) - is UnknownError -> funFailure(Error.UnknownError(error)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/Error.kt b/app/src/main/java/com/base_android_template/shared/network/Error.kt deleted file mode 100644 index 86f05a7..0000000 --- a/app/src/main/java/com/base_android_template/shared/network/Error.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.base_android_template.shared.network - -import java.io.IOException - -sealed class Error { - data class ServerError(val errorResponse: U) : Error() - - data class NetworkError(val error: IOException) : Error() - - data class UnknownError(val exception: Throwable?) : Error() -} - diff --git a/app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapter.kt b/app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapter.kt deleted file mode 100644 index 19f43c8..0000000 --- a/app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.base_android_template.utils.network - -import com.base_android_template.shared.network.ApiResponse -import okhttp3.ResponseBody -import retrofit2.Call -import retrofit2.CallAdapter -import retrofit2.Converter -import java.lang.reflect.Type - -class NetworkResponseAdapter( - private val successType: Type, - private val errorBodyConverter: Converter -) : CallAdapter>> { - - override fun responseType(): Type = successType - - override fun adapt(call: Call): Call> { - return NetworkResponseCall( - call, - errorBodyConverter - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapterFactory.kt b/app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapterFactory.kt deleted file mode 100644 index a4480b6..0000000 --- a/app/src/main/java/com/base_android_template/utils/network/NetworkResponseAdapterFactory.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.base_android_template.utils.network - -import com.base_android_template.shared.network.ApiResponse -import retrofit2.Call -import retrofit2.CallAdapter -import retrofit2.Retrofit -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -class NetworkResponseAdapterFactory : CallAdapter.Factory() { - - override fun get( - returnType: Type, - annotations: Array, - retrofit: Retrofit - ): CallAdapter<*, *>? { - - // suspend functions wrap the response type in `Call` - if (Call::class.java != getRawType(returnType)) { - return null - } - - // check first that the return type is `ParameterizedType` - check(returnType is ParameterizedType) { - "return type must be parameterized as Call> or Call>" - } - - // get the response type inside the `Call` type - val responseType = getParameterUpperBound(0, returnType) - // if the response type is not ApiResponse then we can't handle this type, so we return null - if (getRawType(responseType) != ApiResponse::class.java) { - return null - } - - // the response type is ApiResponse and should be parameterized - check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse or NetworkResponse" } - - val successBodyType = getParameterUpperBound(0, responseType) - val errorBodyType = getParameterUpperBound(1, responseType) - - val errorBodyConverter = - retrofit.nextResponseBodyConverter(null, errorBodyType, annotations) - - return NetworkResponseAdapter( - successBodyType, - errorBodyConverter - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/utils/network/NetworkResponseCall.kt b/app/src/main/java/com/base_android_template/utils/network/NetworkResponseCall.kt deleted file mode 100644 index b54cd49..0000000 --- a/app/src/main/java/com/base_android_template/utils/network/NetworkResponseCall.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.base_android_template.utils.network - -import com.base_android_template.shared.network.ApiResponse -import okhttp3.Request -import okhttp3.ResponseBody -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Converter -import retrofit2.Response -import java.io.EOFException -import java.io.IOException - -internal class NetworkResponseCall( - private val delegate: Call, - private val errorConverter: Converter -) : Call> { - - override fun enqueue(callback: Callback>) { - return delegate.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - var error: E? = null - response.errorBody()?.let { - error = try { - errorConverter.convert(it) - } catch (c: EOFException) { - null - } - } - val body = response.body() - val code = response.code() - - if (response.isSuccessful) { - postSuccessfulResponse(body, callback) - } else { - postFailedResponse(error, callback, code) - } - } - - override fun onFailure(call: Call, throwable: Throwable) { - val networkResponse = when (throwable) { - is IOException -> ApiResponse.NetworkError(throwable) - else -> ApiResponse.UnknownError(throwable) - } - callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse)) - } - - }) - } - - private fun postFailedResponse( - error: E?, - callback: Callback>, - code: Int - ) { - error?.let { - callback.onResponse( - this@NetworkResponseCall, - Response.success(ApiResponse.ServerError(it, code)) - ) - } ?: callback.onResponse( - this@NetworkResponseCall, - Response.success(ApiResponse.UnknownError(null)) - ) - } - - private fun postSuccessfulResponse( - body: S?, - callback: Callback> - ) { - callback.onResponse( - this@NetworkResponseCall, - Response.success(ApiResponse.Success(body)) - ) - } - - override fun isExecuted() = delegate.isExecuted - - override fun clone() = - NetworkResponseCall( - delegate.clone(), - errorConverter - ) - - override fun isCanceled() = delegate.isCanceled - - override fun cancel() = delegate.cancel() - - override fun execute(): Response> { - throw UnsupportedOperationException("NetworkResponseCall doesn't support execute") - } - - override fun request(): Request = delegate.request() -} \ No newline at end of file From 6a5ba9593cb1130cb354fad73569dea3a845ea90 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Thu, 11 Mar 2021 09:37:01 +0200 Subject: [PATCH 09/28] Renamed favorite related classes to Settings. Implemented the UI for language selection in settings screen. --- .../com/base_android_template/di/AppModule.kt | 4 +- .../FavoriteGithubUsersFragment.kt | 13 --- .../FavoriteGithubUsersModule.kt | 8 -- .../FavoriteGithubUsersViewModel.kt | 6 -- .../feature/settings/SettingsFragment.kt | 19 ++++ .../feature/settings/SettingsModule.kt | 8 ++ .../feature/settings/SettingsViewModel.kt | 6 ++ app/src/main/res/drawable/ic_english.xml | 90 +++++++++++++++++++ .../main/res/drawable/ic_favorite_24px.xml | 9 -- .../res/drawable/ic_favorite_border_24px.xml | 9 -- app/src/main/res/drawable/ic_romanian.xml | 24 +++++ .../main/res/drawable/ic_settings_24px.xml | 9 ++ .../layout/fragment_favorite_github_users.xml | 9 -- app/src/main/res/layout/fragment_settings.xml | 69 ++++++++++++++ app/src/main/res/layout/item_github_user.xml | 12 +-- app/src/main/res/menu/bottom_navigation.xml | 6 +- .../main/res/navigation/main_nav_graph.xml | 8 +- app/src/main/res/values-ro/strings.xml | 3 +- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 3 +- 20 files changed, 240 insertions(+), 76 deletions(-) delete mode 100644 app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt delete mode 100644 app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt delete mode 100644 app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt create mode 100644 app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt create mode 100644 app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt create mode 100644 app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt create mode 100644 app/src/main/res/drawable/ic_english.xml delete mode 100644 app/src/main/res/drawable/ic_favorite_24px.xml delete mode 100644 app/src/main/res/drawable/ic_favorite_border_24px.xml create mode 100644 app/src/main/res/drawable/ic_romanian.xml create mode 100644 app/src/main/res/drawable/ic_settings_24px.xml delete mode 100644 app/src/main/res/layout/fragment_favorite_github_users.xml create mode 100644 app/src/main/res/layout/fragment_settings.xml diff --git a/app/src/main/java/com/base_android_template/di/AppModule.kt b/app/src/main/java/com/base_android_template/di/AppModule.kt index 75505f4..a205956 100644 --- a/app/src/main/java/com/base_android_template/di/AppModule.kt +++ b/app/src/main/java/com/base_android_template/di/AppModule.kt @@ -1,6 +1,6 @@ package com.base_android_template.di -import com.base_android_template.feature.favorite_users.favoriteGithubUsersModule +import com.base_android_template.feature.settings.settingsModule import com.base_android_template.feature.github_users.githubUsersModule -fun getAppModules() = createCoreModules() + githubUsersModule + favoriteGithubUsersModule \ No newline at end of file +fun getAppModules() = createCoreModules() + githubUsersModule + settingsModule \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt deleted file mode 100644 index 05ad4ea..0000000 --- a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersFragment.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.base_android_template.feature.favorite_users - -import com.base_android_template.R -import com.base_android_template.base.BaseFragment -import com.base_android_template.databinding.FragmentFavoriteGithubUsersBinding -import org.koin.androidx.viewmodel.ext.android.viewModel - -class FavoriteGithubUsersFragment : - BaseFragment(R.layout.fragment_favorite_github_users) { - - override val viewModel: FavoriteGithubUsersViewModel by viewModel() - -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt deleted file mode 100644 index aad60d9..0000000 --- a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.base_android_template.feature.favorite_users - -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val favoriteGithubUsersModule = module { - viewModel { FavoriteGithubUsersViewModel() } -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt deleted file mode 100644 index 847035f..0000000 --- a/app/src/main/java/com/base_android_template/feature/favorite_users/FavoriteGithubUsersViewModel.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.base_android_template.feature.favorite_users - -import com.base_android_template.base.BaseViewModel - -class FavoriteGithubUsersViewModel : BaseViewModel() { -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt new file mode 100644 index 0000000..9073394 --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt @@ -0,0 +1,19 @@ +package com.base_android_template.feature.settings + +import android.os.Bundle +import android.view.View +import com.base_android_template.R +import com.base_android_template.base.BaseFragment +import com.base_android_template.databinding.FragmentSettingsBinding +import org.koin.androidx.viewmodel.ext.android.viewModel + +class SettingsFragment : + BaseFragment(R.layout.fragment_settings) { + + override val viewModel: SettingsViewModel by viewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt new file mode 100644 index 0000000..fed26b8 --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt @@ -0,0 +1,8 @@ +package com.base_android_template.feature.settings + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val settingsModule = module { + viewModel { SettingsViewModel() } +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt new file mode 100644 index 0000000..e44a9e5 --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt @@ -0,0 +1,6 @@ +package com.base_android_template.feature.settings + +import com.base_android_template.base.BaseViewModel + +class SettingsViewModel : BaseViewModel() { +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_english.xml b/app/src/main/res/drawable/ic_english.xml new file mode 100644 index 0000000..8d7bae4 --- /dev/null +++ b/app/src/main/res/drawable/ic_english.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_favorite_24px.xml b/app/src/main/res/drawable/ic_favorite_24px.xml deleted file mode 100644 index ce351f4..0000000 --- a/app/src/main/res/drawable/ic_favorite_24px.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_favorite_border_24px.xml b/app/src/main/res/drawable/ic_favorite_border_24px.xml deleted file mode 100644 index e664670..0000000 --- a/app/src/main/res/drawable/ic_favorite_border_24px.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_romanian.xml b/app/src/main/res/drawable/ic_romanian.xml new file mode 100644 index 0000000..9a307ad --- /dev/null +++ b/app/src/main/res/drawable/ic_romanian.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_settings_24px.xml b/app/src/main/res/drawable/ic_settings_24px.xml new file mode 100644 index 0000000..282c986 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_favorite_github_users.xml b/app/src/main/res/layout/fragment_favorite_github_users.xml deleted file mode 100644 index 78e83e1..0000000 --- a/app/src/main/res/layout/fragment_favorite_github_users.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..ff8ab3f --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_github_user.xml b/app/src/main/res/layout/item_github_user.xml index 8f4fa69..73ca380 100644 --- a/app/src/main/res/layout/item_github_user.xml +++ b/app/src/main/res/layout/item_github_user.xml @@ -45,21 +45,11 @@ android:textColor="@color/black" android:textSize="@dimen/font_size_sixteen" app:layout_constraintBottom_toBottomOf="@+id/github_user_avatar" - app:layout_constraintEnd_toStartOf="@+id/favorite_user_button" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/github_user_avatar" app:layout_constraintTop_toTopOf="@+id/github_user_avatar" tools:text="Github User" /> - - diff --git a/app/src/main/res/menu/bottom_navigation.xml b/app/src/main/res/menu/bottom_navigation.xml index b43f316..85e9296 100644 --- a/app/src/main/res/menu/bottom_navigation.xml +++ b/app/src/main/res/menu/bottom_navigation.xml @@ -7,9 +7,9 @@ android:title="@string/users" /> + android:id="@+id/settings" + android:icon="@drawable/ic_settings_24px" + android:title="@string/settings" /> diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index f3ef9b7..df2ec3f 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -12,12 +12,12 @@ tools:layout="@layout/fragment_github_users" > + app:destination="@id/settings" /> + android:id="@+id/settings" + android:name="com.base_android_template.feature.settings.SettingsFragment" + android:label="SettingsFragment" /> diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 28b7cfa..d7213b3 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,8 +1,9 @@ + Limba Romana Engleza Useri - Favoriti + Setari Lista cu toti utilizatorii Github, in ordinea in care s-au inregistrat pe GitHub. Aceasta lista include conturi personale de utilizator si conturi de organizatie. \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index c033fa1..815345e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -6,6 +6,7 @@ 8dp 16dp + 24dp 14sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30af72c..df537a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,10 +1,11 @@ Base-Android-Template + Language Romanian English Users - Favorites + Settings Lists all users, in the order that they signed up on GitHub. This list includes personal user accounts and organization accounts. \ No newline at end of file From a5437d693f98ff0bdf2985003b4e2aefc920093d Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Thu, 11 Mar 2021 11:18:49 +0200 Subject: [PATCH 10/28] Implemented single choice selection for language settings. Persist language in Hawk when changing it. Removed language menu in MainActivity's toolbar. Implemented a BindingAdapters method responsible to set from layout the checked changed listener on RadioGroup --- .../java/com/base_android_template/App.kt | 7 ++-- .../com/base_android_template/MainActivity.kt | 25 ------------- .../feature/settings/SettingsFieldsState.kt | 18 ++++++++++ .../feature/settings/SettingsForm.kt | 6 ++++ .../feature/settings/SettingsFragment.kt | 11 ++++-- .../feature/settings/SettingsModule.kt | 2 +- .../feature/settings/SettingsViewModel.kt | 36 ++++++++++++++++++- .../utils/binding_adapter/BindingAdapters.kt | 18 ++++++++++ app/src/main/res/layout/fragment_settings.xml | 14 ++++++-- app/src/main/res/menu/language_menu.xml | 12 ------- 10 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/base_android_template/feature/settings/SettingsFieldsState.kt create mode 100644 app/src/main/java/com/base_android_template/feature/settings/SettingsForm.kt delete mode 100644 app/src/main/res/menu/language_menu.xml diff --git a/app/src/main/java/com/base_android_template/App.kt b/app/src/main/java/com/base_android_template/App.kt index c883b18..7ebce55 100644 --- a/app/src/main/java/com/base_android_template/App.kt +++ b/app/src/main/java/com/base_android_template/App.kt @@ -3,6 +3,7 @@ package com.base_android_template import android.app.Application import android.content.res.Configuration import com.base_android_template.di.getAppModules +import com.base_android_template.shared.HawkKeys import com.base_android_template.shared.Locales import com.base_android_template.utils.language.LocaleUtils import com.orhanobut.hawk.Hawk @@ -22,10 +23,10 @@ class App : Application() { modules(getAppModules()) } - initLocale() - Hawk.init(this).build() + initLocale() + initCalligraphy() } @@ -35,7 +36,7 @@ class App : Application() { } private fun initLocale() { - val locale = Locale(Locales.ENGLISH) + val locale = Locale(Hawk.get(HawkKeys.HAWK_PREF_LOCALE, Locales.ENGLISH)) LocaleUtils.setLocale(locale) LocaleUtils.updateConfig(this, baseContext.resources.configuration) } diff --git a/app/src/main/java/com/base_android_template/MainActivity.kt b/app/src/main/java/com/base_android_template/MainActivity.kt index 3f3cec9..0f16de5 100644 --- a/app/src/main/java/com/base_android_template/MainActivity.kt +++ b/app/src/main/java/com/base_android_template/MainActivity.kt @@ -1,15 +1,11 @@ package com.base_android_template import android.os.Bundle -import android.view.Menu -import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI -import com.base_android_template.shared.Locales import com.base_android_template.utils.language.LocaleUtils import com.google.android.material.bottomnavigation.BottomNavigationView -import java.util.Locale class MainActivity : AppCompatActivity() { @@ -25,27 +21,6 @@ class MainActivity : AppCompatActivity() { setUpNavigation() } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.language_menu, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.english -> { - LocaleUtils.setLocale(Locale(Locales.ENGLISH)) - recreate() - true - } - R.id.romanian -> { - LocaleUtils.setLocale(Locale(Locales.ROMANIAN)) - recreate() - true - } - else -> super.onOptionsItemSelected(item) - } - } - private fun setUpNavigation() { val bottomNavigationView = findViewById(R.id.bottom_navigation) val navHostFragment = supportFragmentManager diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsFieldsState.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsFieldsState.kt new file mode 100644 index 0000000..b5b4827 --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsFieldsState.kt @@ -0,0 +1,18 @@ +package com.base_android_template.feature.settings + +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import com.base_android_template.BR + +data class SettingsFieldsState( + var _englishChecked: Boolean = false +) : + BaseObservable() { + + var englishChecked: Boolean + @Bindable get() = _englishChecked + set(value) { + _englishChecked = value + notifyPropertyChanged(BR.englishChecked) + } +} diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsForm.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsForm.kt new file mode 100644 index 0000000..b3ab538 --- /dev/null +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsForm.kt @@ -0,0 +1,6 @@ +package com.base_android_template.feature.settings + +class SettingsForm { + + val fields = SettingsFieldsState() +} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt index 9073394..76dd210 100644 --- a/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsFragment.kt @@ -1,7 +1,6 @@ package com.base_android_template.feature.settings import android.os.Bundle -import android.view.View import com.base_android_template.R import com.base_android_template.base.BaseFragment import com.base_android_template.databinding.FragmentSettingsBinding @@ -12,8 +11,14 @@ class SettingsFragment : override val viewModel: SettingsViewModel by viewModel() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel.recreateActivity.observe(viewLifecycleOwner, { + if (it) { + activity?.recreate() + } + }) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt index fed26b8..e964f4c 100644 --- a/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsModule.kt @@ -4,5 +4,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val settingsModule = module { - viewModel { SettingsViewModel() } + viewModel { SettingsViewModel(preferencesProvider = get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt b/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt index e44a9e5..2dd27ee 100644 --- a/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/base_android_template/feature/settings/SettingsViewModel.kt @@ -1,6 +1,40 @@ package com.base_android_template.feature.settings +import android.widget.RadioGroup +import androidx.core.view.get +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.base_android_template.base.BaseViewModel +import com.base_android_template.shared.Locales +import com.base_android_template.shared.provider.PreferencesProvider +import com.base_android_template.utils.language.LocaleUtils +import java.util.Locale + +class SettingsViewModel(private val preferencesProvider: PreferencesProvider) : BaseViewModel() { + + val settingsForm = SettingsForm() + val recreateActivity: LiveData get() = _recreateActivity + + private val _recreateActivity = MutableLiveData() + + init { + settingsForm.fields.englishChecked = preferencesProvider.getPrefLocale() == Locales.ENGLISH + } + + val languageRadioGroupListener: RadioGroup.OnCheckedChangeListener = + RadioGroup.OnCheckedChangeListener { radioGroup, _ -> + when (radioGroup.checkedRadioButtonId) { + radioGroup[0].id -> changeLanguage(Locales.ENGLISH) + else -> changeLanguage(Locales.ROMANIAN) + } + } + + private fun changeLanguage(language: String) { + LocaleUtils.setLocale(Locale(language)) + preferencesProvider.setPrefLocale(language) + settingsForm.fields.englishChecked = language == Locales.ENGLISH + _recreateActivity.value = true + _recreateActivity.value = false + } -class SettingsViewModel : BaseViewModel() { } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt b/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt index 8a5e1c2..00d1bbe 100644 --- a/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt +++ b/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt @@ -1,6 +1,7 @@ package com.base_android_template.utils.binding_adapter import android.widget.ImageView +import android.widget.RadioGroup import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.RecyclerView @@ -51,4 +52,21 @@ object BindingAdapters { .apply(options) .into(imageView) } + + /** + * Set the RadioGroup.OnCheckedChangeListener on RadioGroup view + * + * @param radioGroup RadioGroup. The RadioGroup to which the OnCheckedChangeListener + * will be attached + * @param listener RadioGroup.OnCheckedChangeListener. The listener that will be + * invoked when the checked radio button changed in this group + */ + @BindingAdapter("app:setRadioGroupListener") + @JvmStatic + fun setRadioGroupListener( + radioGroup: RadioGroup, + listener: RadioGroup.OnCheckedChangeListener + ) { + radioGroup.setOnCheckedChangeListener(listener) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index ff8ab3f..1681c1e 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -2,6 +2,13 @@ + + + + + + app:layout_constraintTop_toTopOf="parent" + app:setRadioGroupListener="@{viewModel.languageRadioGroupListener}"> @@ -53,8 +62,9 @@ diff --git a/app/src/main/res/menu/language_menu.xml b/app/src/main/res/menu/language_menu.xml deleted file mode 100644 index 0f8148b..0000000 --- a/app/src/main/res/menu/language_menu.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file From 1e04499c6d4fa0e248d1dd24861449ceb50cedd9 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Thu, 11 Mar 2021 15:23:21 +0200 Subject: [PATCH 11/28] Set full screen app mode --- app/src/main/AndroidManifest.xml | 4 +-- app/src/main/res/values-night/themes.xml | 24 ++++++++++-------- app/src/main/res/values-v27/themes.xml | 18 +++++++++++++ app/src/main/res/values/themes.xml | 25 ++++++++++++------- .../fragment_github_users_motion_scene.xml | 19 ++++++++++++++ 5 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 app/src/main/res/values-v27/themes.xml create mode 100644 app/src/main/res/xml/fragment_github_users_motion_scene.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c5e3c3..a5db077 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,13 +6,13 @@ + android:theme="@style/AppTheme.FullScreen"> diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 685ba4b..33f4ba8 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,12 +1,14 @@ - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml new file mode 100644 index 0000000..482afd3 --- /dev/null +++ b/app/src/main/res/values-v27/themes.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 71d2c5e..26a707c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,12 +1,19 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/xml/fragment_github_users_motion_scene.xml b/app/src/main/res/xml/fragment_github_users_motion_scene.xml new file mode 100644 index 0000000..87cfb01 --- /dev/null +++ b/app/src/main/res/xml/fragment_github_users_motion_scene.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file From d361d84ca3469181a5b2494f0836bd6e821fff89 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Fri, 12 Mar 2021 08:59:27 +0200 Subject: [PATCH 12/28] Changed the UI of Github users list screen by adding a title above the message. Animate using MotionLayout the title and the message when scrolling the users list. --- app/src/main/res/drawable/bg_shadow.xml | 24 +++ .../main/res/layout/fragment_github_users.xml | 71 +++++++-- app/src/main/res/layout/fragment_settings.xml | 6 +- app/src/main/res/values-ro/strings.xml | 3 +- app/src/main/res/values-v27/themes.xml | 2 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/values/themes.xml | 2 +- .../fragment_github_users_motion_scene.xml | 139 ++++++++++++++++-- 10 files changed, 227 insertions(+), 28 deletions(-) create mode 100644 app/src/main/res/drawable/bg_shadow.xml diff --git a/app/src/main/res/drawable/bg_shadow.xml b/app/src/main/res/drawable/bg_shadow.xml new file mode 100644 index 0000000..4049149 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_github_users.xml b/app/src/main/res/layout/fragment_github_users.xml index 80efc03..bc57ee4 100644 --- a/app/src/main/res/layout/fragment_github_users.xml +++ b/app/src/main/res/layout/fragment_github_users.xml @@ -1,5 +1,7 @@ - + @@ -8,35 +10,80 @@ type="com.base_android_template.feature.github_users.GithubUsersViewModel" /> - + android:layout_height="match_parent" + app:layoutDescription="@xml/fragment_github_users_motion_scene"> + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/welcome_title" /> + + + + + + app:layout_constraintTop_toBottomOf="@+id/top_separator" + tools:listitem="@layout/item_github_user" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 1681c1e..d9cbe5f 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -11,14 +11,16 @@ + android:layout_height="match_parent"> diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d7213b3..8264b0c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -5,5 +5,6 @@ Useri Setari - Lista cu toti utilizatorii Github, in ordinea in care s-au inregistrat pe GitHub. Aceasta lista include conturi personale de utilizator si conturi de organizatie. + 👋 Salut, bine ai venit in aplicatia Base Android Template! + Pentru a demonstra apelurile catre server si operatiile pe baza de date Room, vom afisa in acest ecran o lista cu 30 de utilizatori Github preluati din baza de date locala. Daca baza de date nu contine utilizatori, lista va fi preluata de la server si apoi va fi salvata in baza de date locala. \ No newline at end of file diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml index 482afd3..a8c6e8a 100644 --- a/app/src/main/res/values-v27/themes.xml +++ b/app/src/main/res/values-v27/themes.xml @@ -2,7 +2,7 @@ --> + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml deleted file mode 100644 index a8c6e8a..0000000 --- a/app/src/main/res/values-v27/themes.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7a6b6bc..1344323 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -16,4 +16,8 @@ true + + \ No newline at end of file From 4584c3046ae64046f9ae8e36c9b32bf27e56b3aa Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Mon, 15 Mar 2021 14:05:55 +0200 Subject: [PATCH 14/28] Implemented a general way to display messages in the app. Handled the exceptions inside GithubUsersViewModel --- .../base/BaseFragment.kt | 12 +++++++++++ .../base/BaseViewModel.kt | 11 ++++++++++ .../github_users/GithubUsersFragment.kt | 16 ++++++++++++++ .../github_users/GithubUsersViewModel.kt | 21 +++++++++++++------ .../shared/network/Exception.kt | 2 -- .../usecase/GetGithubUsersUseCaseImpl.kt | 8 +++++-- app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 62 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/base_android_template/base/BaseFragment.kt b/app/src/main/java/com/base_android_template/base/BaseFragment.kt index 981b033..e2ce7c0 100644 --- a/app/src/main/java/com/base_android_template/base/BaseFragment.kt +++ b/app/src/main/java/com/base_android_template/base/BaseFragment.kt @@ -4,7 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment @@ -46,6 +48,15 @@ abstract class BaseFragment(@LayoutRes } } + private val messageObserver = Observer { + (activity as? AppCompatActivity)?.apply { + if (it.isNotEmpty()) { + Toast.makeText(this, it, Toast.LENGTH_LONG).show() + viewModel.clearLastMessage() + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -69,6 +80,7 @@ abstract class BaseFragment(@LayoutRes viewModel.apply { navigationCommand.observe(viewLifecycleOwner, commandObserver) loading.observe(viewLifecycleOwner, loadingObserver) + message.observe(viewLifecycleOwner, messageObserver) } } diff --git a/app/src/main/java/com/base_android_template/base/BaseViewModel.kt b/app/src/main/java/com/base_android_template/base/BaseViewModel.kt index 3d3f953..33eb56b 100644 --- a/app/src/main/java/com/base_android_template/base/BaseViewModel.kt +++ b/app/src/main/java/com/base_android_template/base/BaseViewModel.kt @@ -12,9 +12,12 @@ open class BaseViewModel : ViewModel() { get() = _navigationCommand val loading: LiveData get() = _loading + val message: LiveData + get() = _message private val _navigationCommand = MutableLiveData() private val _loading = MutableLiveData() + private val _message = MutableLiveData() fun postNavigationCommand(direction: NavDirections) { _navigationCommand.postValue(NavigationCommand.PerformNavAction(direction)) @@ -31,4 +34,12 @@ open class BaseViewModel : ViewModel() { fun postLoading(loading: Boolean) { _loading.postValue(loading) } + + fun postMessage(toDisplay: String) { + _message.postValue(toDisplay) + } + + fun clearLastMessage() { + _message.value = "" + } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt index 25190e4..d422d43 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt @@ -1,8 +1,10 @@ package com.base_android_template.feature.github_users +import android.os.Bundle import com.base_android_template.R import com.base_android_template.base.BaseFragment import com.base_android_template.databinding.FragmentGithubUsersBinding +import com.base_android_template.shared.network.Exception import org.koin.androidx.viewmodel.ext.android.viewModel class GithubUsersFragment : @@ -10,4 +12,18 @@ class GithubUsersFragment : override val viewModel: GithubUsersViewModel by viewModel() + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel.exception.observe(viewLifecycleOwner, { + when (it) { + is Exception.EmptyLocalGithubUsersLisException -> { + viewModel.getRemoteGithubUsers() + } + else -> { + viewModel.postMessage(getString(R.string.error_fetching_users_list)) + } + } + }) + } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt index ea1b9ad..696209b 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt @@ -1,10 +1,13 @@ package com.base_android_template.feature.github_users +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.base_android_template.base.BaseViewModel import com.base_android_template.usecase.GetGithubUsersUseCase import kotlinx.coroutines.launch import timber.log.Timber +import com.base_android_template.shared.network.Exception class GithubUsersViewModel( private val getGithubUsersUseCase: GetGithubUsersUseCase @@ -12,6 +15,9 @@ class GithubUsersViewModel( BaseViewModel() { val githubUsersListAdapter = GithubUsersListAdapter() + val exception: LiveData get() = _exception + + private val _exception = MutableLiveData() init { getLocalCartItems() @@ -22,7 +28,7 @@ class GithubUsersViewModel( viewModelScope.launch { getGithubUsersUseCase.getLocalGithubUsers().fold( { - getRemoteGithubUsers() + handleException(it) }, { githubUsersListAdapter.submitList(it) @@ -32,15 +38,18 @@ class GithubUsersViewModel( } } - private fun getRemoteGithubUsers() { + private fun handleException(exception: Exception) { + Timber.d(exception.toString()) + postLoading(false) + _exception.value = exception + } + + fun getRemoteGithubUsers() { viewModelScope.launch { getGithubUsersUseCase.getRemoteAndSaveLocalGithubUsers().fold( { postLoading(false) - Timber.d( - GithubUsersViewModel::class.simpleName, - "Error fetching Github users list" - ) + handleException(it) }, { githubUsersListAdapter.submitList(it) diff --git a/app/src/main/java/com/base_android_template/shared/network/Exception.kt b/app/src/main/java/com/base_android_template/shared/network/Exception.kt index 628e64b..6468b3a 100644 --- a/app/src/main/java/com/base_android_template/shared/network/Exception.kt +++ b/app/src/main/java/com/base_android_template/shared/network/Exception.kt @@ -5,7 +5,5 @@ sealed class Exception { object ServerException : Exception() object SaveGithubUsersException : Exception() object EmptyLocalGithubUsersLisException : Exception() - data class ClientException(val errorMessage: String) : Exception() data class UnknownException(val exception: Throwable) : Exception() - object DataBaseException : Exception() } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt index 9ff5e3d..563b23a 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt @@ -3,10 +3,10 @@ package com.base_android_template.usecase import com.base_android_template.model.entity.GithubUserEntity import com.base_android_template.persistance.dao.GithubUsersListDao import com.base_android_template.repository.GithubUsersRepository +import com.base_android_template.shared.network.Exception import com.base_android_template.utils.Either import com.base_android_template.utils.doOnSuccess import com.base_android_template.utils.map -import com.base_android_template.shared.network.Exception import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -35,7 +35,11 @@ class GetGithubUsersUseCaseImpl internal constructor( } private suspend fun updateLocalGithubUsersList(list: List) { - githubUsersListDao.insertGithubUsers(list) + try { + githubUsersListDao.insertGithubUsers(list) + } catch (e: kotlin.Exception) { + Either.Failure(Exception.SaveGithubUsersException) + } } } \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 8264b0c..e5c75d9 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -7,4 +7,5 @@ Setari 👋 Salut, bine ai venit in aplicatia Base Android Template! Pentru a demonstra apelurile catre server si operatiile pe baza de date Room, vom afisa in acest ecran o lista cu 30 de utilizatori Github preluati din baza de date locala. Daca baza de date nu contine utilizatori, lista va fi preluata de la server si apoi va fi salvata in baza de date locala. + Ceva nu a functionat! Lista cu utilizatori Github nu a putut fi preluata. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66ff02f..45ba5c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Settings 👋 Hi, welcome to Base Android Template! To prove server calls and Room database operations, we will display in this screen a list of 30 Github users fetched from the local database. If the database does not contain users, the list will be retrieved from the server and then saved in the local database. + Something went wrong! The Github users list could not be fetched. \ No newline at end of file From 7d5ee9caaa8d20d64fbd6f3909ef665f714cbacc Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 17 Mar 2021 13:00:32 +0200 Subject: [PATCH 15/28] Increased the version code of some libraries --- app/build.gradle | 4 +--- build.gradle | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index eb044d0..d06a24f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,11 +59,9 @@ dependencies { //Retrofit implementation "com.squareup.retrofit2:retrofit:$retrofit" + implementation "com.squareup.retrofit2:converter-gson:$retrofit" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp" - // Gson - implementation "com.squareup.retrofit2:converter-gson:$gson" - //Glide implementation "com.github.bumptech.glide:glide:$glide" kapt "com.github.bumptech.glide:compiler:$glide" diff --git a/build.gradle b/build.gradle index 838e2d0..5e77c09 100644 --- a/build.gradle +++ b/build.gradle @@ -8,17 +8,16 @@ buildscript { constraint_layout = "2.0.4" junit = "1.1.2" espresso = "3.3.0" - navigationVersion = "2.3.3" + navigationVersion = "2.3.4" room_version = "2.2.6" calligraphy = "3.1.1" viewPump = "2.0.3" timber = "4.7.1" hawk = "2.0.1" glide = "4.11.0" - gson = "2.6.1" - okhttp = "3.11.0" - retrofit = "2.6.1" - koin = "2.0.1" + okhttp = "4.9.0" + retrofit = "2.7.2" + koin = "2.2.2" } repositories { From a76fa2ab4cd240ce88b6aece7c02dedf62da3549 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 17 Mar 2021 13:00:51 +0200 Subject: [PATCH 16/28] Removed unused BaseActivity class --- .../java/com/base_android_template/base/BaseActivity.kt | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/src/main/java/com/base_android_template/base/BaseActivity.kt diff --git a/app/src/main/java/com/base_android_template/base/BaseActivity.kt b/app/src/main/java/com/base_android_template/base/BaseActivity.kt deleted file mode 100644 index 347ada5..0000000 --- a/app/src/main/java/com/base_android_template/base/BaseActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.base_android_template.base - -import androidx.appcompat.app.AppCompatActivity - -class BaseActivity : AppCompatActivity() { -} \ No newline at end of file From d54647f63cb869cfcf69ef75bb6f7e488ea19bb0 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 17 Mar 2021 13:15:45 +0200 Subject: [PATCH 17/28] Commented the BaseFragment class --- .../base/BaseFragment.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/base_android_template/base/BaseFragment.kt b/app/src/main/java/com/base_android_template/base/BaseFragment.kt index e2ce7c0..31cf7cc 100644 --- a/app/src/main/java/com/base_android_template/base/BaseFragment.kt +++ b/app/src/main/java/com/base_android_template/base/BaseFragment.kt @@ -17,14 +17,25 @@ import com.base_android_template.shared.loading.UILoading import com.base_android_template.shared.model.NavigationCommand import org.koin.android.ext.android.inject +/** + * Represents the base class that will be extended by any Fragment in the app. + * It receives the corresponding ViewDataBinding and ViewModel generic types + * and the id of the layout to be inflated + * + * @param layoutResId Int. The id of the layout that defines the structure + * for the user interface of the fragment + */ abstract class BaseFragment(@LayoutRes private val layoutResId: Int) : Fragment() { private var binding: VB? = null protected abstract val viewModel: VM - private val loadingDialog: UILoading by inject() + /** + * Receives the value from loading LiveData variable inside BaseViewModel + * and display the loading progress bar if value is true, and hide it otherwise + */ private val loadingObserver: Observer = Observer { showLoading -> if (showLoading) { loadingDialog.show() @@ -34,6 +45,10 @@ abstract class BaseFragment(@LayoutRes loadingDialog.hide() } + /** + * Receives the value from navigationCommand LiveData variable inside BaseViewModel + * and, depending on NavigationCommand, proceed the navigation + */ private val commandObserver = Observer { command -> if (command != null) { when (command) { @@ -48,6 +63,10 @@ abstract class BaseFragment(@LayoutRes } } + /** + * Receives the value from message LiveData variable inside BaseViewModel + * and displays the messages in a Toast + */ private val messageObserver = Observer { (activity as? AppCompatActivity)?.apply { if (it.isNotEmpty()) { @@ -63,6 +82,10 @@ abstract class BaseFragment(@LayoutRes context?.let { loadingDialog.init(it) } } + /** + * Using DataBindingUtil, inflate the layout by creating the corresponding ViewDataBinding + * Set always used "viewModel" DataBinding variable + */ override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,6 +107,9 @@ abstract class BaseFragment(@LayoutRes } } + /** + * Access the binding object from all subclasses + */ protected fun requireBinding(): VB = binding ?: throw NullPointerException("View is in destroyed state and the Binding is null") From 99f6bc62cdebbc29d82eb2c48187aa847df708b3 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 17 Mar 2021 13:26:49 +0200 Subject: [PATCH 18/28] Commented BaseViewModel class and methods --- .../base/BaseViewModel.kt | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/base_android_template/base/BaseViewModel.kt b/app/src/main/java/com/base_android_template/base/BaseViewModel.kt index 33eb56b..ff24ff0 100644 --- a/app/src/main/java/com/base_android_template/base/BaseViewModel.kt +++ b/app/src/main/java/com/base_android_template/base/BaseViewModel.kt @@ -6,6 +6,12 @@ import androidx.lifecycle.ViewModel import androidx.navigation.NavDirections import com.base_android_template.shared.model.NavigationCommand +/** + * Represents the base class that will be extended by any ViewModel in the app. + * It contains public methods that can be called from all ViewModels in the hierarchy + * to send commands to the BaseFragment + * + */ open class BaseViewModel : ViewModel() { val navigationCommand: LiveData @@ -19,10 +25,20 @@ open class BaseViewModel : ViewModel() { private val _loading = MutableLiveData() private val _message = MutableLiveData() + /** + * Call this method when want to navigate between fragments via NavDirections + * + * @param direction NavDirections. The generated method for navigation action + * that you've defined in the nav graph + */ fun postNavigationCommand(direction: NavDirections) { _navigationCommand.postValue(NavigationCommand.PerformNavAction(direction)) } + /** + * Call this method when want to navigate up in fragments + * + */ fun postNavigateUpCommand() { _navigationCommand.postValue(NavigationCommand.PerformNavUp) } @@ -31,10 +47,11 @@ open class BaseViewModel : ViewModel() { _navigationCommand.value = null } - fun postLoading(loading: Boolean) { - _loading.postValue(loading) - } - + /** + * Call this method when want to display a message in a Toast + * + * @param toDisplay String. The message to be displayed + */ fun postMessage(toDisplay: String) { _message.postValue(toDisplay) } @@ -42,4 +59,16 @@ open class BaseViewModel : ViewModel() { fun clearLastMessage() { _message.value = "" } + + /** + * Call this method when want to show/hide the loading progress bar + * + * @param loading Boolean. If: + * - true: show the loading progress bar + * - false: hide th loading progress bar + */ + fun postLoading(loading: Boolean) { + _loading.postValue(loading) + } + } \ No newline at end of file From 74beeb652c0e1dd479a83a5a239b76245ac30b4b Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Wed, 17 Mar 2021 14:07:48 +0200 Subject: [PATCH 19/28] Cleaned the code --- .../github_users/GithubUsersListViewHolder.kt | 6 +- .../github_users/GithubUsersViewModel.kt | 16 ++-- .../model/entity/GithubUserEntity.kt | 74 ++++++++++--------- .../shared/model/NavigationCommand.kt | 2 + .../utils/binding_adapter/BindingAdapters.kt | 6 +- .../utils/language/LocaleUtils.kt | 2 +- 6 files changed, 57 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt index ebc92a9..7db0150 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersListViewHolder.kt @@ -8,7 +8,9 @@ class GithubUsersListViewHolder(private val binding: ItemGithubUserBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(githubUserItem: GithubUserEntity) { - binding.item = githubUserItem - binding.executePendingBindings() + binding.apply { + item = githubUserItem + executePendingBindings() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt index 696209b..854858d 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt @@ -4,10 +4,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.base_android_template.base.BaseViewModel +import com.base_android_template.shared.network.Exception import com.base_android_template.usecase.GetGithubUsersUseCase import kotlinx.coroutines.launch import timber.log.Timber -import com.base_android_template.shared.network.Exception class GithubUsersViewModel( private val getGithubUsersUseCase: GetGithubUsersUseCase @@ -28,7 +28,7 @@ class GithubUsersViewModel( viewModelScope.launch { getGithubUsersUseCase.getLocalGithubUsers().fold( { - handleException(it) + handleException(it) }, { githubUsersListAdapter.submitList(it) @@ -38,12 +38,6 @@ class GithubUsersViewModel( } } - private fun handleException(exception: Exception) { - Timber.d(exception.toString()) - postLoading(false) - _exception.value = exception - } - fun getRemoteGithubUsers() { viewModelScope.launch { getGithubUsersUseCase.getRemoteAndSaveLocalGithubUsers().fold( @@ -58,4 +52,10 @@ class GithubUsersViewModel( ) } } + + private fun handleException(exception: Exception) { + Timber.d(exception.toString()) + postLoading(false) + _exception.value = exception + } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt b/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt index a918874..ff9f5b2 100644 --- a/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt +++ b/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt @@ -1,70 +1,72 @@ package com.base_android_template.model.entity +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import com.base_android_template.model.response.GithubUserResponse -import com.google.gson.annotations.SerializedName @Entity( primaryKeys = ["id"], indices = [Index("id")] ) data class GithubUserEntity( - @field:SerializedName("login") - val login: String = "", + @ColumnInfo(name = "login") + val login: String, - @field:SerializedName("id") - val id: Int = 0, + @ColumnInfo(name = "id") + val id: Int, - @field:SerializedName("node_id") - val nodeId: String = "", + @ColumnInfo(name = "node_id") + val nodeId: String, - @field:SerializedName("avatar_url") - val avatarUrl: String = "", + @ColumnInfo(name = "avatar_url") + val avatarUrl: String, - @field:SerializedName("gravatar_id") - val grAvatarId: String = "", + @ColumnInfo(name = "gravatar_id") + val grAvatarId: String, - @field:SerializedName("url") - val url: String = "", + @ColumnInfo(name = "url") + val url: String, - @field:SerializedName("html_url") - val htmlUrl: String = "", + @ColumnInfo(name = "html_url") + val htmlUrl: String, - @field:SerializedName("followers_url") - val followersUrl: String = "", + @ColumnInfo(name = "followers_url") + val followersUrl: String, - @field:SerializedName("following_url") - val followingUrl: String = "", + @ColumnInfo(name = "following_url") + val followingUrl: String, - @field:SerializedName("gists_url") - val gistsUrl: String = "", + @ColumnInfo(name = "gists_url") + val gistsUrl: String, - @field:SerializedName("starred_url") - val starredUrl: String = "", + @ColumnInfo(name = "starred_url") + val starredUrl: String, - @field:SerializedName("subscriptions_url") - val subscriptionsUrl: String = "", + @ColumnInfo(name = "subscriptions_url") + val subscriptionsUrl: String, - @field:SerializedName("organizations_url") - val organizationsUrl: String = "", + @ColumnInfo(name = "organizations_url") + val organizationsUrl: String, - @field:SerializedName("repos_url") - val reposUrl: String = "", + @ColumnInfo(name = "repos_url") + val reposUrl: String, - @field:SerializedName("events_url") - val eventsUrl: String = "", + @ColumnInfo(name = "events_url") + val eventsUrl: String, - @field:SerializedName("received_events_url") - val receivedEventsUrl: String = "", + @ColumnInfo(name = "received_events_url") + val receivedEventsUrl: String, - @field:SerializedName("type") - val type: String = "", + @ColumnInfo(name = "type") + val type: String, - @field:SerializedName("site_admin") + @ColumnInfo(name = "site_admin") val siteAdmin: String = "" ) { + companion object { + fun mapToGithubUserEntity(githubUserResponse: GithubUserResponse) = GithubUserEntity( login = githubUserResponse.login.orEmpty(), diff --git a/app/src/main/java/com/base_android_template/shared/model/NavigationCommand.kt b/app/src/main/java/com/base_android_template/shared/model/NavigationCommand.kt index a219377..d37d393 100644 --- a/app/src/main/java/com/base_android_template/shared/model/NavigationCommand.kt +++ b/app/src/main/java/com/base_android_template/shared/model/NavigationCommand.kt @@ -3,6 +3,8 @@ package com.base_android_template.shared.model import androidx.navigation.NavDirections sealed class NavigationCommand { + object PerformNavUp : NavigationCommand() + data class PerformNavAction(val direction: NavDirections) : NavigationCommand() } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt b/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt index 00d1bbe..4d0ae6c 100644 --- a/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt +++ b/app/src/main/java/com/base_android_template/utils/binding_adapter/BindingAdapters.kt @@ -10,8 +10,10 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions -/* - * Binding adapters are responsible for making the appropriate framework calls to set values for views directly inside layout. +/** + * BindingAdapters class is responsible for making the appropriate framework calls to + * set values for views directly inside layout. + * */ object BindingAdapters { diff --git a/app/src/main/java/com/base_android_template/utils/language/LocaleUtils.kt b/app/src/main/java/com/base_android_template/utils/language/LocaleUtils.kt index 8ba44a7..79d6f78 100644 --- a/app/src/main/java/com/base_android_template/utils/language/LocaleUtils.kt +++ b/app/src/main/java/com/base_android_template/utils/language/LocaleUtils.kt @@ -6,7 +6,7 @@ import android.content.res.Resources import android.view.ContextThemeWrapper import java.util.Locale -/* +/** * Utility class to change app locale settings. */ From d3c0e2db34ff1bf5172984926c4dfdeee5f94e99 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Tue, 23 Mar 2021 11:41:55 +0200 Subject: [PATCH 20/28] Removed unused ressources --- app/src/main/java/com/base_android_template/di/ApiModule.kt | 5 ++--- app/src/main/res/values/colors.xml | 1 - app/src/main/res/values/dimens.xml | 1 - app/src/main/res/values/themes.xml | 5 ----- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/base_android_template/di/ApiModule.kt b/app/src/main/java/com/base_android_template/di/ApiModule.kt index 058c6b1..f7aa937 100644 --- a/app/src/main/java/com/base_android_template/di/ApiModule.kt +++ b/app/src/main/java/com/base_android_template/di/ApiModule.kt @@ -7,9 +7,8 @@ import org.koin.dsl.module import retrofit2.Retrofit val apiModule = module { - fun provideUserApi(retrofit: Retrofit): GithubUsersApi { - return retrofit.create(GithubUsersApi::class.java) - } + fun provideUserApi(retrofit: Retrofit): GithubUsersApi = + retrofit.create(GithubUsersApi::class.java) single { GithubUsersListRemoteImpl( diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2cb0776..cf54358 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,5 @@ - #FF90CAF9 #FF2196F3 #FF1976D2 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 48e5577..9b83971 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -10,7 +10,6 @@ 32dp - 14sp 16sp 24sp \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1344323..64609f5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -11,11 +11,6 @@ shortEdges - - From 42825a5c40359c5b69f344e63581a4bad1121392 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Fri, 26 Mar 2021 16:54:27 +0200 Subject: [PATCH 21/28] Removed allowBackup property in AndroidManifest --- app/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5db077..fe7b0ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ Date: Fri, 26 Mar 2021 16:56:53 +0200 Subject: [PATCH 22/28] Implemented ExceptionHandler as interface and created its implementation --- .../com/base_android_template/di/CoreModules.kt | 3 ++- .../shared/network/ExceptionHandler.kt | 8 ++------ .../shared/network/ExceptionHandlerImpl.kt | 13 +++++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt diff --git a/app/src/main/java/com/base_android_template/di/CoreModules.kt b/app/src/main/java/com/base_android_template/di/CoreModules.kt index c4e8034..749d77a 100644 --- a/app/src/main/java/com/base_android_template/di/CoreModules.kt +++ b/app/src/main/java/com/base_android_template/di/CoreModules.kt @@ -7,6 +7,7 @@ import com.base_android_template.shared.BASE_URL import com.base_android_template.shared.loading.UILoading import com.base_android_template.shared.loading.UILoadingImplementation import com.base_android_template.shared.network.ExceptionHandler +import com.base_android_template.shared.network.ExceptionHandlerImpl import com.base_android_template.shared.network.NetworkHandler import com.base_android_template.shared.provider.PreferencesProvider import com.base_android_template.shared.provider.PreferencesProviderImpl @@ -42,7 +43,7 @@ val coreModules = module { factory { UILoadingImplementation() } - single { ExceptionHandler() } + single { ExceptionHandlerImpl() } fun provideNetworkHandler(context: Context): NetworkHandler = NetworkHandler(context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager) diff --git a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt index 0365a05..06fb521 100644 --- a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt +++ b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt @@ -1,12 +1,8 @@ package com.base_android_template.shared.network import com.base_android_template.utils.Either -import java.net.UnknownHostException -class ExceptionHandler { +interface ExceptionHandler { - fun handleGeneralException(exception: Throwable): Either.Failure = when (exception) { - is UnknownHostException -> Either.Failure(Exception.NetworkException) - else -> Either.Failure(Exception.UnknownException(exception)) - } + fun handleGeneralException(exception: Throwable): Either.Failure } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt new file mode 100644 index 0000000..6af5875 --- /dev/null +++ b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt @@ -0,0 +1,13 @@ +package com.base_android_template.shared.network + +import com.base_android_template.utils.Either +import java.net.UnknownHostException + +class ExceptionHandlerImpl : ExceptionHandler { + + override fun handleGeneralException(exception: Throwable): Either.Failure = + when (exception) { + is UnknownHostException -> Either.Failure(Exception.NetworkException) + else -> Either.Failure(Exception.UnknownException(exception)) + } +} \ No newline at end of file From 779112801e462a5de485f83a2ec7b21374e3046a Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Fri, 26 Mar 2021 17:02:15 +0200 Subject: [PATCH 23/28] Removed NetworkHandler class because it doesn't make sense to check if there is an active network connection before making the call. If we want to know if the error occurred because of no network, we can check that when getting 404 response --- .../com/base_android_template/di/ApiModule.kt | 1 - .../base_android_template/di/CoreModules.kt | 9 ----- .../remote/GithubUsersListRemoteImpl.kt | 5 --- .../shared/network/NetworkHandler.kt | 35 ------------------- 4 files changed, 50 deletions(-) delete mode 100644 app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt diff --git a/app/src/main/java/com/base_android_template/di/ApiModule.kt b/app/src/main/java/com/base_android_template/di/ApiModule.kt index f7aa937..0d21385 100644 --- a/app/src/main/java/com/base_android_template/di/ApiModule.kt +++ b/app/src/main/java/com/base_android_template/di/ApiModule.kt @@ -13,7 +13,6 @@ val apiModule = module { single { GithubUsersListRemoteImpl( githubUsersListService = get(), - networkHandler = get(), exceptionHandler = get() ) } diff --git a/app/src/main/java/com/base_android_template/di/CoreModules.kt b/app/src/main/java/com/base_android_template/di/CoreModules.kt index 749d77a..23ba68f 100644 --- a/app/src/main/java/com/base_android_template/di/CoreModules.kt +++ b/app/src/main/java/com/base_android_template/di/CoreModules.kt @@ -1,14 +1,10 @@ package com.base_android_template.di -import android.content.Context -import android.content.Context.CONNECTIVITY_SERVICE -import android.net.ConnectivityManager import com.base_android_template.shared.BASE_URL import com.base_android_template.shared.loading.UILoading import com.base_android_template.shared.loading.UILoadingImplementation import com.base_android_template.shared.network.ExceptionHandler import com.base_android_template.shared.network.ExceptionHandlerImpl -import com.base_android_template.shared.network.NetworkHandler import com.base_android_template.shared.provider.PreferencesProvider import com.base_android_template.shared.provider.PreferencesProviderImpl import com.google.gson.FieldNamingPolicy @@ -45,11 +41,6 @@ val coreModules = module { single { ExceptionHandlerImpl() } - fun provideNetworkHandler(context: Context): NetworkHandler = - NetworkHandler(context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager) - - single { provideNetworkHandler(get()) } - } fun createCoreModules() = diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt index abdd136..e71a34b 100644 --- a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt @@ -4,12 +4,10 @@ import com.base_android_template.model.response.GithubUserResponse import com.base_android_template.utils.Either import com.base_android_template.shared.network.Exception import com.base_android_template.shared.network.ExceptionHandler -import com.base_android_template.shared.network.NetworkHandler import retrofit2.Response class GithubUsersListRemoteImpl( private val githubUsersListService: GithubUsersApi, - private val networkHandler: NetworkHandler, private val exceptionHandler: ExceptionHandler ) : GithubUsersListRemote { @@ -18,9 +16,6 @@ class GithubUsersListRemoteImpl( } private suspend fun makeRequest(block: suspend () -> Response>): Either> { - if (!networkHandler.hasNetworkConnection()) { - return Either.Failure(Exception.NetworkException) - } return try { val response = block.invoke() handleResponse(response) diff --git a/app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt b/app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt deleted file mode 100644 index 1fc0c12..0000000 --- a/app/src/main/java/com/base_android_template/shared/network/NetworkHandler.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.base_android_template.shared.network - -import android.Manifest.permission.ACCESS_NETWORK_STATE -import android.net.ConnectivityManager -import android.net.NetworkCapabilities -import android.net.NetworkInfo -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission - -class NetworkHandler constructor(private val connectivityManager: ConnectivityManager) { - - @RequiresPermission(ACCESS_NETWORK_STATE) - fun hasNetworkConnection(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val networkConnection = - connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - hasNetworkConnection(networkConnection) - } else { - val networkInfo = connectivityManager.activeNetworkInfo - hasNetworkConnection(networkInfo) - } - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private fun hasNetworkConnection(networkCapabilities: NetworkCapabilities?): Boolean { - return networkCapabilities != null && (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_CELLULAR - )) - } - - private fun hasNetworkConnection(networkInfo: NetworkInfo?): Boolean { - return networkInfo != null && networkInfo.isConnected - } -} \ No newline at end of file From b3c9610fddcc31ea847cf6e1b0e0f883391c6019 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Mon, 29 Mar 2021 08:42:54 +0300 Subject: [PATCH 24/28] Renamed EmptyLocalGithubUsersLisException to EmptyLocalGithubUsersListException --- .../feature/github_users/GithubUsersFragment.kt | 2 +- .../java/com/base_android_template/shared/network/Exception.kt | 2 +- .../base_android_template/usecase/GetGithubUsersUseCaseImpl.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt index d422d43..942be25 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt @@ -17,7 +17,7 @@ class GithubUsersFragment : viewModel.exception.observe(viewLifecycleOwner, { when (it) { - is Exception.EmptyLocalGithubUsersLisException -> { + is Exception.EmptyLocalGithubUsersListException -> { viewModel.getRemoteGithubUsers() } else -> { diff --git a/app/src/main/java/com/base_android_template/shared/network/Exception.kt b/app/src/main/java/com/base_android_template/shared/network/Exception.kt index 6468b3a..4f76e95 100644 --- a/app/src/main/java/com/base_android_template/shared/network/Exception.kt +++ b/app/src/main/java/com/base_android_template/shared/network/Exception.kt @@ -4,6 +4,6 @@ sealed class Exception { object NetworkException : Exception() object ServerException : Exception() object SaveGithubUsersException : Exception() - object EmptyLocalGithubUsersLisException : Exception() + object EmptyLocalGithubUsersListException : Exception() data class UnknownException(val exception: Throwable) : Exception() } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt index 563b23a..48e115e 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt @@ -30,7 +30,7 @@ class GetGithubUsersUseCaseImpl internal constructor( val list = githubUsersListDao.getGithubUsers() return if (list?.isNullOrEmpty() == true) - Either.Failure(Exception.EmptyLocalGithubUsersLisException) + Either.Failure(Exception.EmptyLocalGithubUsersListException) else Either.Success(list) } From 8a037fcddebc6192c1a440ef2295051e84543452 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Mon, 29 Mar 2021 10:12:24 +0300 Subject: [PATCH 25/28] Removed Exception class and implemented a sealed class to handle only the Github users errors to handle in this way the errors by usecases --- .../com/base_android_template/di/ApiModule.kt | 3 +-- .../base_android_template/di/CoreModules.kt | 5 ----- .../github_users/GithubUsersFragment.kt | 6 +++--- .../github_users/GithubUsersViewModel.kt | 12 +++++------ .../remote/GithubUsersListRemote.kt | 4 ++-- .../remote/GithubUsersListRemoteImpl.kt | 21 +++++++++---------- .../repository/GithubUsersRepository.kt | 4 ++-- .../repository/GithubUsersRepositoryImpl.kt | 4 ++-- .../shared/network/Exception.kt | 9 -------- .../shared/network/ExceptionHandler.kt | 8 ------- .../shared/network/ExceptionHandlerImpl.kt | 13 ------------ .../usecase/GetGithubUsersUseCase.kt | 5 ++--- .../usecase/GetGithubUsersUseCaseImpl.kt | 13 ++++-------- .../usecase/GithubUsersError.kt | 6 ++++++ 14 files changed, 38 insertions(+), 75 deletions(-) delete mode 100644 app/src/main/java/com/base_android_template/shared/network/Exception.kt delete mode 100644 app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt delete mode 100644 app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt create mode 100644 app/src/main/java/com/base_android_template/usecase/GithubUsersError.kt diff --git a/app/src/main/java/com/base_android_template/di/ApiModule.kt b/app/src/main/java/com/base_android_template/di/ApiModule.kt index 0d21385..bd04cb3 100644 --- a/app/src/main/java/com/base_android_template/di/ApiModule.kt +++ b/app/src/main/java/com/base_android_template/di/ApiModule.kt @@ -12,8 +12,7 @@ val apiModule = module { single { GithubUsersListRemoteImpl( - githubUsersListService = get(), - exceptionHandler = get() + githubUsersListService = get() ) } diff --git a/app/src/main/java/com/base_android_template/di/CoreModules.kt b/app/src/main/java/com/base_android_template/di/CoreModules.kt index 23ba68f..f76d4a1 100644 --- a/app/src/main/java/com/base_android_template/di/CoreModules.kt +++ b/app/src/main/java/com/base_android_template/di/CoreModules.kt @@ -3,8 +3,6 @@ package com.base_android_template.di import com.base_android_template.shared.BASE_URL import com.base_android_template.shared.loading.UILoading import com.base_android_template.shared.loading.UILoadingImplementation -import com.base_android_template.shared.network.ExceptionHandler -import com.base_android_template.shared.network.ExceptionHandlerImpl import com.base_android_template.shared.provider.PreferencesProvider import com.base_android_template.shared.provider.PreferencesProviderImpl import com.google.gson.FieldNamingPolicy @@ -38,9 +36,6 @@ val coreModules = module { factory { PreferencesProviderImpl() } factory { UILoadingImplementation() } - - single { ExceptionHandlerImpl() } - } fun createCoreModules() = diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt index 942be25..ec8c56c 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt @@ -4,7 +4,7 @@ import android.os.Bundle import com.base_android_template.R import com.base_android_template.base.BaseFragment import com.base_android_template.databinding.FragmentGithubUsersBinding -import com.base_android_template.shared.network.Exception +import com.base_android_template.usecase.GithubUsersError import org.koin.androidx.viewmodel.ext.android.viewModel class GithubUsersFragment : @@ -15,9 +15,9 @@ class GithubUsersFragment : override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel.exception.observe(viewLifecycleOwner, { + viewModel.githubUsersError.observe(viewLifecycleOwner, { when (it) { - is Exception.EmptyLocalGithubUsersListException -> { + is GithubUsersError.EmptyLocalGithubUsersListException -> { viewModel.getRemoteGithubUsers() } else -> { diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt index 854858d..f875788 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.base_android_template.base.BaseViewModel -import com.base_android_template.shared.network.Exception import com.base_android_template.usecase.GetGithubUsersUseCase +import com.base_android_template.usecase.GithubUsersError import kotlinx.coroutines.launch import timber.log.Timber @@ -15,9 +15,9 @@ class GithubUsersViewModel( BaseViewModel() { val githubUsersListAdapter = GithubUsersListAdapter() - val exception: LiveData get() = _exception + val githubUsersError: LiveData get() = _githubUsersError - private val _exception = MutableLiveData() + private val _githubUsersError = MutableLiveData() init { getLocalCartItems() @@ -53,9 +53,9 @@ class GithubUsersViewModel( } } - private fun handleException(exception: Exception) { - Timber.d(exception.toString()) + private fun handleException(githubUsersError: GithubUsersError) { + Timber.d(this.githubUsersError.toString()) postLoading(false) - _exception.value = exception + _githubUsersError.value = githubUsersError } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt index 29aac79..96d8087 100644 --- a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemote.kt @@ -2,10 +2,10 @@ package com.base_android_template.remote import com.base_android_template.model.response.GithubUserResponse import com.base_android_template.utils.Either -import com.base_android_template.shared.network.Exception +import com.base_android_template.usecase.GithubUsersError interface GithubUsersListRemote { - suspend fun getGithubUsersList(): Either> + suspend fun getGithubUsersList(): Either> } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt index e71a34b..ad9841f 100644 --- a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt @@ -1,34 +1,33 @@ package com.base_android_template.remote import com.base_android_template.model.response.GithubUserResponse +import com.base_android_template.usecase.GithubUsersError import com.base_android_template.utils.Either -import com.base_android_template.shared.network.Exception -import com.base_android_template.shared.network.ExceptionHandler import retrofit2.Response class GithubUsersListRemoteImpl( - private val githubUsersListService: GithubUsersApi, - private val exceptionHandler: ExceptionHandler + private val githubUsersListService: GithubUsersApi ) : GithubUsersListRemote { - override suspend fun getGithubUsersList(): Either> = makeRequest { - githubUsersListService.getGithubUsers() - } + override suspend fun getGithubUsersList(): Either> = + makeRequest { + githubUsersListService.getGithubUsers() + } - private suspend fun makeRequest(block: suspend () -> Response>): Either> { + private suspend fun makeRequest(block: suspend () -> Response>): Either> { return try { val response = block.invoke() handleResponse(response) } catch (exception: Throwable) { - exceptionHandler.handleGeneralException(exception) + Either.Failure(GithubUsersError.GeneralError) } } - private fun handleResponse(response: Response>): Either> { + private fun handleResponse(response: Response>): Either> { return if (response.isSuccessful) { handleSuccess(response) } else { - Either.Failure(Exception.ServerException) + Either.Failure(GithubUsersError.GeneralError) } } diff --git a/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt b/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt index 64b4842..487eb58 100644 --- a/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt +++ b/app/src/main/java/com/base_android_template/repository/GithubUsersRepository.kt @@ -2,9 +2,9 @@ package com.base_android_template.repository import com.base_android_template.model.response.GithubUserResponse import com.base_android_template.utils.Either -import com.base_android_template.shared.network.Exception +import com.base_android_template.usecase.GithubUsersError interface GithubUsersRepository { - suspend fun getLocalGithubUsers(): Either> + suspend fun getLocalGithubUsers(): Either> } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt b/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt index 47e7928..8f3b968 100644 --- a/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt +++ b/app/src/main/java/com/base_android_template/repository/GithubUsersRepositoryImpl.kt @@ -3,13 +3,13 @@ package com.base_android_template.repository import com.base_android_template.model.response.GithubUserResponse import com.base_android_template.remote.GithubUsersListRemote import com.base_android_template.utils.Either -import com.base_android_template.shared.network.Exception +import com.base_android_template.usecase.GithubUsersError class GithubUsersRepositoryImpl( private val githubUsersListRemote: GithubUsersListRemote ) : GithubUsersRepository { - override suspend fun getLocalGithubUsers(): Either> = + override suspend fun getLocalGithubUsers(): Either> = githubUsersListRemote.getGithubUsersList() } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/Exception.kt b/app/src/main/java/com/base_android_template/shared/network/Exception.kt deleted file mode 100644 index 4f76e95..0000000 --- a/app/src/main/java/com/base_android_template/shared/network/Exception.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.base_android_template.shared.network - -sealed class Exception { - object NetworkException : Exception() - object ServerException : Exception() - object SaveGithubUsersException : Exception() - object EmptyLocalGithubUsersListException : Exception() - data class UnknownException(val exception: Throwable) : Exception() -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt deleted file mode 100644 index 06fb521..0000000 --- a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandler.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.base_android_template.shared.network - -import com.base_android_template.utils.Either - -interface ExceptionHandler { - - fun handleGeneralException(exception: Throwable): Either.Failure -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt b/app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt deleted file mode 100644 index 6af5875..0000000 --- a/app/src/main/java/com/base_android_template/shared/network/ExceptionHandlerImpl.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.base_android_template.shared.network - -import com.base_android_template.utils.Either -import java.net.UnknownHostException - -class ExceptionHandlerImpl : ExceptionHandler { - - override fun handleGeneralException(exception: Throwable): Either.Failure = - when (exception) { - is UnknownHostException -> Either.Failure(Exception.NetworkException) - else -> Either.Failure(Exception.UnknownException(exception)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt index 35ddc2c..0908ea2 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCase.kt @@ -2,11 +2,10 @@ package com.base_android_template.usecase import com.base_android_template.model.entity.GithubUserEntity import com.base_android_template.utils.Either -import com.base_android_template.shared.network.Exception interface GetGithubUsersUseCase { - suspend fun getRemoteAndSaveLocalGithubUsers(): Either> + suspend fun getRemoteAndSaveLocalGithubUsers(): Either> - suspend fun getLocalGithubUsers(): Either> + suspend fun getLocalGithubUsers(): Either> } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt index 48e115e..9f0d0d5 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt @@ -3,7 +3,6 @@ package com.base_android_template.usecase import com.base_android_template.model.entity.GithubUserEntity import com.base_android_template.persistance.dao.GithubUsersListDao import com.base_android_template.repository.GithubUsersRepository -import com.base_android_template.shared.network.Exception import com.base_android_template.utils.Either import com.base_android_template.utils.doOnSuccess import com.base_android_template.utils.map @@ -15,7 +14,7 @@ class GetGithubUsersUseCaseImpl internal constructor( private val githubUsersListDao: GithubUsersListDao ) : GetGithubUsersUseCase { - override suspend fun getRemoteAndSaveLocalGithubUsers(): Either> = + override suspend fun getRemoteAndSaveLocalGithubUsers(): Either> = coroutineScope { githubUsersRepository.getLocalGithubUsers() .map { githubUsersList -> @@ -26,20 +25,16 @@ class GetGithubUsersUseCaseImpl internal constructor( } } - override suspend fun getLocalGithubUsers(): Either> { + override suspend fun getLocalGithubUsers(): Either> { val list = githubUsersListDao.getGithubUsers() return if (list?.isNullOrEmpty() == true) - Either.Failure(Exception.EmptyLocalGithubUsersListException) + Either.Failure(GithubUsersError.EmptyLocalGithubUsersListException) else Either.Success(list) } private suspend fun updateLocalGithubUsersList(list: List) { - try { - githubUsersListDao.insertGithubUsers(list) - } catch (e: kotlin.Exception) { - Either.Failure(Exception.SaveGithubUsersException) - } + githubUsersListDao.insertGithubUsers(list) } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GithubUsersError.kt b/app/src/main/java/com/base_android_template/usecase/GithubUsersError.kt new file mode 100644 index 0000000..ea53842 --- /dev/null +++ b/app/src/main/java/com/base_android_template/usecase/GithubUsersError.kt @@ -0,0 +1,6 @@ +package com.base_android_template.usecase + +sealed class GithubUsersError { + object EmptyLocalGithubUsersListException : GithubUsersError() + object GeneralError :GithubUsersError() +} \ No newline at end of file From 0f2120451aa04abf8af59c5a7b62dd8925a37e0f Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Mon, 29 Mar 2021 10:14:15 +0300 Subject: [PATCH 26/28] Catch only java.io.IOException to avoid a too general catch. --- .../base_android_template/remote/GithubUsersListRemoteImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt index ad9841f..46d4802 100644 --- a/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt +++ b/app/src/main/java/com/base_android_template/remote/GithubUsersListRemoteImpl.kt @@ -4,6 +4,7 @@ import com.base_android_template.model.response.GithubUserResponse import com.base_android_template.usecase.GithubUsersError import com.base_android_template.utils.Either import retrofit2.Response +import java.io.IOException class GithubUsersListRemoteImpl( private val githubUsersListService: GithubUsersApi @@ -18,7 +19,7 @@ class GithubUsersListRemoteImpl( return try { val response = block.invoke() handleResponse(response) - } catch (exception: Throwable) { + } catch (exception: IOException) { Either.Failure(GithubUsersError.GeneralError) } } From 02bcc9e4344a985a4016f83186ca0e2198eec6c1 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Mon, 29 Mar 2021 10:47:05 +0300 Subject: [PATCH 27/28] Implemented a general way to display the messages directly from ViewModel. Moved the logic inside GithubUsersFragment -> onActivitCreated() to GithubUserViewModel. Implemented the SingleLiveEvent class to not clear every time the live data value --- .../base/BaseFragment.kt | 17 ++++- .../base/BaseViewModel.kt | 29 ++++---- .../github_users/GithubUsersFragment.kt | 17 ----- .../github_users/GithubUsersViewModel.kt | 23 ++++--- .../utils/SingleLiveEvent.kt | 68 +++++++++++++++++++ 5 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/base_android_template/utils/SingleLiveEvent.kt diff --git a/app/src/main/java/com/base_android_template/base/BaseFragment.kt b/app/src/main/java/com/base_android_template/base/BaseFragment.kt index 31cf7cc..0bb9bd7 100644 --- a/app/src/main/java/com/base_android_template/base/BaseFragment.kt +++ b/app/src/main/java/com/base_android_template/base/BaseFragment.kt @@ -59,7 +59,6 @@ abstract class BaseFragment(@LayoutRes findNavController().navigate(command.direction) } } - viewModel.clearLastNavigationCommand() } } @@ -71,7 +70,20 @@ abstract class BaseFragment(@LayoutRes (activity as? AppCompatActivity)?.apply { if (it.isNotEmpty()) { Toast.makeText(this, it, Toast.LENGTH_LONG).show() - viewModel.clearLastMessage() + } + } + } + + /** + * Receives the value from messageResId LiveData variable inside BaseViewModel + * and get the corresponding string to the id and displays the messages in a Toast + */ + private val messageResIdObserver = Observer { + (activity as? AppCompatActivity)?.apply { + if (it != -1) { + if (getString(it) != "") { + Toast.makeText(this, it, Toast.LENGTH_LONG).show() + } } } } @@ -104,6 +116,7 @@ abstract class BaseFragment(@LayoutRes navigationCommand.observe(viewLifecycleOwner, commandObserver) loading.observe(viewLifecycleOwner, loadingObserver) message.observe(viewLifecycleOwner, messageObserver) + messageResId.observe(viewLifecycleOwner, messageResIdObserver) } } diff --git a/app/src/main/java/com/base_android_template/base/BaseViewModel.kt b/app/src/main/java/com/base_android_template/base/BaseViewModel.kt index ff24ff0..87dc77e 100644 --- a/app/src/main/java/com/base_android_template/base/BaseViewModel.kt +++ b/app/src/main/java/com/base_android_template/base/BaseViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.navigation.NavDirections import com.base_android_template.shared.model.NavigationCommand +import com.base_android_template.utils.SingleLiveEvent /** * Represents the base class that will be extended by any ViewModel in the app. @@ -14,16 +15,19 @@ import com.base_android_template.shared.model.NavigationCommand */ open class BaseViewModel : ViewModel() { - val navigationCommand: LiveData + val navigationCommand: SingleLiveEvent get() = _navigationCommand - val loading: LiveData + val loading: SingleLiveEvent get() = _loading - val message: LiveData + val message: SingleLiveEvent get() = _message + val messageResId: SingleLiveEvent + get() = _messageResId - private val _navigationCommand = MutableLiveData() - private val _loading = MutableLiveData() - private val _message = MutableLiveData() + private val _navigationCommand = SingleLiveEvent() + private val _loading = SingleLiveEvent() + private val _message = SingleLiveEvent() + private val _messageResId = SingleLiveEvent() /** * Call this method when want to navigate between fragments via NavDirections @@ -43,10 +47,6 @@ open class BaseViewModel : ViewModel() { _navigationCommand.postValue(NavigationCommand.PerformNavUp) } - fun clearLastNavigationCommand() { - _navigationCommand.value = null - } - /** * Call this method when want to display a message in a Toast * @@ -56,8 +56,13 @@ open class BaseViewModel : ViewModel() { _message.postValue(toDisplay) } - fun clearLastMessage() { - _message.value = "" + /** + * Call this method when want to display a message in a Toast + * + * @param id Int. The id of the string to be displayed + */ + fun postMessageResId(id: Int) { + _messageResId.postValue(id) } /** diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt index ec8c56c..a16c5e4 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersFragment.kt @@ -1,29 +1,12 @@ package com.base_android_template.feature.github_users -import android.os.Bundle import com.base_android_template.R import com.base_android_template.base.BaseFragment import com.base_android_template.databinding.FragmentGithubUsersBinding -import com.base_android_template.usecase.GithubUsersError import org.koin.androidx.viewmodel.ext.android.viewModel class GithubUsersFragment : BaseFragment(R.layout.fragment_github_users) { override val viewModel: GithubUsersViewModel by viewModel() - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - viewModel.githubUsersError.observe(viewLifecycleOwner, { - when (it) { - is GithubUsersError.EmptyLocalGithubUsersListException -> { - viewModel.getRemoteGithubUsers() - } - else -> { - viewModel.postMessage(getString(R.string.error_fetching_users_list)) - } - } - }) - } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt index f875788..f012bf8 100644 --- a/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt +++ b/app/src/main/java/com/base_android_template/feature/github_users/GithubUsersViewModel.kt @@ -1,13 +1,11 @@ package com.base_android_template.feature.github_users -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.base_android_template.R import com.base_android_template.base.BaseViewModel import com.base_android_template.usecase.GetGithubUsersUseCase import com.base_android_template.usecase.GithubUsersError import kotlinx.coroutines.launch -import timber.log.Timber class GithubUsersViewModel( private val getGithubUsersUseCase: GetGithubUsersUseCase @@ -15,9 +13,6 @@ class GithubUsersViewModel( BaseViewModel() { val githubUsersListAdapter = GithubUsersListAdapter() - val githubUsersError: LiveData get() = _githubUsersError - - private val _githubUsersError = MutableLiveData() init { getLocalCartItems() @@ -38,7 +33,7 @@ class GithubUsersViewModel( } } - fun getRemoteGithubUsers() { + private fun getRemoteGithubUsers() { viewModelScope.launch { getGithubUsersUseCase.getRemoteAndSaveLocalGithubUsers().fold( { @@ -54,8 +49,18 @@ class GithubUsersViewModel( } private fun handleException(githubUsersError: GithubUsersError) { - Timber.d(this.githubUsersError.toString()) postLoading(false) - _githubUsersError.value = githubUsersError + handleGithubUsersError(githubUsersError) + } + + private fun handleGithubUsersError(githubUsersError: GithubUsersError) { + when (githubUsersError) { + is GithubUsersError.EmptyLocalGithubUsersListException -> { + getRemoteGithubUsers() + } + else -> { + postMessageResId(R.string.error_fetching_users_list) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/utils/SingleLiveEvent.kt b/app/src/main/java/com/base_android_template/utils/SingleLiveEvent.kt new file mode 100644 index 0000000..53f2258 --- /dev/null +++ b/app/src/main/java/com/base_android_template/utils/SingleLiveEvent.kt @@ -0,0 +1,68 @@ +package com.base_android_template.utils + +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.util.Log +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + * + * + * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + * + * + * Note that only one observer is going to be notified of changes. + */ +class SingleLiveEvent : MutableLiveData() { + private val pending = AtomicBoolean(false) + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") + } + // Observe the internal MutableLiveData + super.observe(owner, Observer { t -> + if (pending.compareAndSet(true, false)) { + observer.onChanged(t) + } + }) + } + + @MainThread + override fun setValue(t: T?) { + pending.set(true) + super.setValue(t) + } + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } + companion object { + private val TAG = "SingleLiveEvent" + } +} \ No newline at end of file From 9266a8bf9dad04a92536cecf4647edf96cd8d3a9 Mon Sep 17 00:00:00 2001 From: Loredana Petrea Date: Mon, 29 Mar 2021 11:13:41 +0300 Subject: [PATCH 28/28] Implemented an extension function for GithubUserResponse to map it to GithubUserEntity --- .../model/entity/GithubUserEntity.kt | 29 +------------------ .../usecase/GetGithubUsersUseCaseImpl.kt | 3 +- .../extensions/GithubUserResponseExtesion.kt | 26 +++++++++++++++++ 3 files changed, 29 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/com/base_android_template/utils/extensions/GithubUserResponseExtesion.kt diff --git a/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt b/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt index ff9f5b2..8db17f6 100644 --- a/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt +++ b/app/src/main/java/com/base_android_template/model/entity/GithubUserEntity.kt @@ -3,7 +3,6 @@ package com.base_android_template.model.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index -import com.base_android_template.model.response.GithubUserResponse @Entity( primaryKeys = ["id"], @@ -63,30 +62,4 @@ data class GithubUserEntity( @ColumnInfo(name = "site_admin") val siteAdmin: String = "" -) { - - companion object { - - fun mapToGithubUserEntity(githubUserResponse: GithubUserResponse) = - GithubUserEntity( - login = githubUserResponse.login.orEmpty(), - id = githubUserResponse.id, - nodeId = githubUserResponse.nodeId.orEmpty(), - avatarUrl = githubUserResponse.avatarUrl.orEmpty(), - grAvatarId = githubUserResponse.grAvatarId.orEmpty(), - url = githubUserResponse.url.orEmpty(), - htmlUrl = githubUserResponse.htmlUrl.orEmpty(), - followersUrl = githubUserResponse.followersUrl.orEmpty(), - followingUrl = githubUserResponse.followingUrl.orEmpty(), - gistsUrl = githubUserResponse.gistsUrl.orEmpty(), - starredUrl = githubUserResponse.starredUrl.orEmpty(), - subscriptionsUrl = githubUserResponse.subscriptionsUrl.orEmpty(), - organizationsUrl = githubUserResponse.organizationsUrl.orEmpty(), - reposUrl = githubUserResponse.reposUrl.orEmpty(), - eventsUrl = githubUserResponse.eventsUrl.orEmpty(), - receivedEventsUrl = githubUserResponse.receivedEventsUrl.orEmpty(), - type = githubUserResponse.type.orEmpty(), - siteAdmin = githubUserResponse.siteAdmin.orEmpty() - ) - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt index 9f0d0d5..071bd5d 100644 --- a/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt +++ b/app/src/main/java/com/base_android_template/usecase/GetGithubUsersUseCaseImpl.kt @@ -5,6 +5,7 @@ import com.base_android_template.persistance.dao.GithubUsersListDao import com.base_android_template.repository.GithubUsersRepository import com.base_android_template.utils.Either import com.base_android_template.utils.doOnSuccess +import com.base_android_template.utils.extensions.mapToGithubUserEntity import com.base_android_template.utils.map import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ class GetGithubUsersUseCaseImpl internal constructor( coroutineScope { githubUsersRepository.getLocalGithubUsers() .map { githubUsersList -> - githubUsersList.map { GithubUserEntity.mapToGithubUserEntity(it) } + githubUsersList.map { it.mapToGithubUserEntity() } } .doOnSuccess { launch { updateLocalGithubUsersList(it) } diff --git a/app/src/main/java/com/base_android_template/utils/extensions/GithubUserResponseExtesion.kt b/app/src/main/java/com/base_android_template/utils/extensions/GithubUserResponseExtesion.kt new file mode 100644 index 0000000..ce33efc --- /dev/null +++ b/app/src/main/java/com/base_android_template/utils/extensions/GithubUserResponseExtesion.kt @@ -0,0 +1,26 @@ +package com.base_android_template.utils.extensions + +import com.base_android_template.model.entity.GithubUserEntity +import com.base_android_template.model.response.GithubUserResponse + +fun GithubUserResponse.mapToGithubUserEntity(): GithubUserEntity = + GithubUserEntity( + login = this.login.orEmpty(), + id = this.id, + nodeId = this.nodeId.orEmpty(), + avatarUrl = this.avatarUrl.orEmpty(), + grAvatarId = this.grAvatarId.orEmpty(), + url = this.url.orEmpty(), + htmlUrl = this.htmlUrl.orEmpty(), + followersUrl = this.followersUrl.orEmpty(), + followingUrl = this.followingUrl.orEmpty(), + gistsUrl = this.gistsUrl.orEmpty(), + starredUrl = this.starredUrl.orEmpty(), + subscriptionsUrl = this.subscriptionsUrl.orEmpty(), + organizationsUrl = this.organizationsUrl.orEmpty(), + reposUrl = this.reposUrl.orEmpty(), + eventsUrl = this.eventsUrl.orEmpty(), + receivedEventsUrl = this.receivedEventsUrl.orEmpty(), + type = this.type.orEmpty(), + siteAdmin = this.siteAdmin + ) \ No newline at end of file