This is a repository that contains the source code of my submissions project at Dicoding "Belajar Fundamental Aplikasi Android" course, start from the first submission until the final submission. This course is a part of self-paced learning at Bangkit 2022 Mobile Development learning path. I try to implement the best practices of the Kotlin programming language and Android framework to this project.
3 |
4 | ## Disclaimer ⚠️
5 | This repository is created for sharing and educational purposes only. Plagiarism is unacceptable and is not my responsibility as the author.
6 |
7 | ## Submission 1: GitHub User App
8 | * Rating: ⭐⭐⭐⭐⭐
9 | * There are some Android components that I implement in this submission project:
10 | * Intent
11 | * Parcelable
12 | * RecyclerView
13 | * ConstraintLayout
14 | * What I learned from this submission:
15 | * Writing documentation with KDoc syntax
16 | * How to implement Kotlin's scope function in an Android project
17 | * Implement Kotlin's coding convention standard
18 | * Implement Dark and Light mode in Android apps
19 | * Screenshot of the application
20 |
21 |
22 | ## Submission 2: GitHub User App
23 | * Rating: ⭐⭐⭐⭐⭐
24 | * There are some Android components that I implement in this submission project:
25 | * Retrofit
26 | * SearchView
27 | * CoordinatorLayout
28 | * TabLayout
29 | * What I learned from this submission:
30 | * Managing memory leaks with LeakCanary
31 | * How to store an API KEY in the right place
32 | * How to using CoordinatorLayout to improve UX in the detail user page
33 | * Writing an Extensions for a ImageView, this simplify the process to render an image with Glide library
34 | * How ViewModel and LiveData actually works.
35 | * Screenshot of the application
36 |
37 |
38 | ## Submission 3: GitHub User App
39 | * Rating: ⭐⭐⭐⭐⭐
40 | * There are some Android components that I implement in this submission project:
41 | * Room
42 | * DataStore
43 | * Espresso
44 | * What I learned from this submission:
45 | * How to implement Repository Pattern with Kotlin Flow
46 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | id 'kotlin-parcelize'
6 | id 'dagger.hilt.android.plugin'
7 | }
8 |
9 | android {
10 | compileSdk 31
11 |
12 | defaultConfig {
13 | applicationId "com.artworkspace.github"
14 | minSdk 23
15 | targetSdk 31
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 |
21 | def localProperties = new Properties()
22 | localProperties.load(new FileInputStream(rootProject.file("local.properties")))
23 |
24 | buildConfigField("String", "API_KEY", "\"" + localProperties['apiKey'] + "\"")
25 | }
26 |
27 | buildTypes {
28 | release {
29 | minifyEnabled false
30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
31 | }
32 | }
33 |
34 | buildFeatures {
35 | viewBinding true
36 | }
37 |
38 | compileOptions {
39 | sourceCompatibility JavaVersion.VERSION_1_8
40 | targetCompatibility JavaVersion.VERSION_1_8
41 | }
42 | kotlinOptions {
43 | jvmTarget = '1.8'
44 | }
45 | }
46 |
47 | dependencies {
48 |
49 | // Android
50 | implementation 'androidx.core:core-ktx:1.7.0'
51 | implementation "androidx.activity:activity-ktx:1.4.0"
52 | implementation "androidx.fragment:fragment-ktx:1.4.1"
53 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
54 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
55 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
56 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
57 | implementation 'androidx.appcompat:appcompat:1.4.1'
58 | implementation 'com.google.android.material:material:1.5.0'
59 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
60 | implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
61 | implementation "androidx.viewpager2:viewpager2:1.0.0"
62 |
63 | // Hilt
64 | implementation 'com.google.dagger:hilt-android:2.41'
65 | kapt 'com.google.dagger:hilt-compiler:2.41'
66 |
67 | // Room
68 | implementation 'androidx.room:room-runtime:2.4.2'
69 | implementation "androidx.room:room-ktx:2.4.2"
70 | kapt 'androidx.room:room-compiler:2.4.2'
71 |
72 | // Circular Image View
73 | implementation 'de.hdodenhof:circleimageview:3.1.0'
74 |
75 | // Glide
76 | implementation 'com.github.bumptech.glide:glide:4.13.0'
77 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
78 | annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0'
79 |
80 | // Retrofit
81 | implementation 'com.github.bumptech.glide:glide:4.13.0'
82 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
83 | implementation "com.squareup.retrofit2:converter-gson:2.9.0"
84 | implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
85 |
86 | // DataStore
87 | implementation "androidx.datastore:datastore-preferences:1.0.0"
88 |
89 | // Testing
90 | implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
91 | testImplementation 'junit:junit:4.13.2'
92 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
93 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
94 | androidTestImplementation 'androidx.test:runner:1.4.0'
95 | androidTestImplementation 'androidx.test:rules:1.4.0'
96 | androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
97 |
98 | // LeakCanary
99 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
100 | }
101 |
102 | kapt {
103 | correctErrorTypes true
104 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/artworkspace/github/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("com.artworkspace.github", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/artworkspace/github/ui/view/FavoriteActivityTest.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import androidx.test.core.app.ActivityScenario
4 | import androidx.test.espresso.Espresso.onView
5 | import androidx.test.espresso.Espresso.pressBack
6 | import androidx.test.espresso.IdlingRegistry
7 | import androidx.test.espresso.action.ViewActions.click
8 | import androidx.test.espresso.assertion.ViewAssertions.matches
9 | import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
10 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
11 | import androidx.test.espresso.matcher.ViewMatchers.withId
12 | import com.artworkspace.github.R
13 | import com.artworkspace.github.adapter.ListUserAdapter
14 | import com.artworkspace.github.utils.EspressoIdlingResource
15 | import org.junit.After
16 | import org.junit.Before
17 | import org.junit.Test
18 |
19 | class FavoriteActivityTest {
20 |
21 | @Before
22 | fun setUp() {
23 | IdlingRegistry.getInstance().register(EspressoIdlingResource.idlingResource)
24 | ActivityScenario.launch(FavoriteActivity::class.java)
25 | }
26 |
27 | @After
28 | fun tearDown() {
29 | IdlingRegistry.getInstance().unregister(EspressoIdlingResource.idlingResource)
30 | }
31 |
32 | @Test
33 | fun testDeleteAllFavoriteUser() {
34 | onView(withId(R.id.rv_favorite)).check(matches(isDisplayed()))
35 |
36 | while (true) {
37 | try {
38 | onView(withId(R.id.rv_favorite)).perform(
39 | actionOnItemAtPosition(0, click())
40 | )
41 |
42 | onView(withId(R.id.user_detail_container)).check(matches(isDisplayed()))
43 | onView(withId(R.id.tabs)).check(matches(isDisplayed()))
44 | onView(withId(R.id.fab_favorite)).check(matches(isDisplayed()))
45 |
46 | onView(withId(R.id.fab_favorite)).perform(click())
47 | pressBack()
48 | } catch (e: Exception) {
49 | break
50 | }
51 | }
52 |
53 | onView(withId(R.id.tv_message)).check(matches(isDisplayed()))
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/artworkspace/github/ui/view/MainActivityTest.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.content.res.Resources
4 | import android.view.KeyEvent
5 | import androidx.test.core.app.ActivityScenario
6 | import androidx.test.espresso.Espresso.onView
7 | import androidx.test.espresso.Espresso.pressBack
8 | import androidx.test.espresso.IdlingRegistry
9 | import androidx.test.espresso.action.ViewActions.*
10 | import androidx.test.espresso.assertion.ViewAssertions.matches
11 | import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
12 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
13 | import androidx.test.espresso.matcher.ViewMatchers.withId
14 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
15 | import com.artworkspace.github.R
16 | import com.artworkspace.github.adapter.ListUserAdapter
17 | import com.artworkspace.github.utils.EspressoIdlingResource
18 | import org.junit.After
19 | import org.junit.Before
20 | import org.junit.Test
21 | import org.junit.runner.RunWith
22 |
23 | @RunWith(AndroidJUnit4ClassRunner::class)
24 | class MainActivityTest {
25 | @Before
26 | fun setup() {
27 | IdlingRegistry.getInstance().register(EspressoIdlingResource.idlingResource)
28 | ActivityScenario.launch(MainActivity::class.java)
29 | }
30 |
31 | @After
32 | fun tearDown() {
33 | IdlingRegistry.getInstance().unregister(EspressoIdlingResource.idlingResource)
34 | }
35 |
36 | @Test
37 | fun testComponentShowCorrectly() {
38 | onView(withId(R.id.toolbar_home)).check(matches(isDisplayed()))
39 | onView(withId(R.id.tv_result_count)).check(matches(isDisplayed()))
40 | onView(withId(R.id.rv_users)).check(matches(isDisplayed()))
41 | }
42 |
43 | @Test
44 | fun testSelectFirstUser() {
45 | onView(withId(R.id.rv_users)).check(matches(isDisplayed()))
46 | onView(withId(R.id.rv_users)).perform(
47 | actionOnItemAtPosition(
48 | 0,
49 | click()
50 | )
51 | )
52 |
53 | onView(withId(R.id.user_detail_container)).check(matches(isDisplayed()))
54 | onView(withId(R.id.tabs)).check(matches(isDisplayed()))
55 | onView(withId(R.id.fab_favorite)).check(matches(isDisplayed()))
56 | }
57 |
58 | @Test
59 | fun testAddUserToFavorite() {
60 | onView(withId(R.id.rv_users)).check(matches(isDisplayed()))
61 | onView(withId(R.id.rv_users)).perform(
62 | actionOnItemAtPosition(
63 | 0,
64 | click()
65 | )
66 | )
67 |
68 | onView(withId(R.id.user_detail_container)).check(matches(isDisplayed()))
69 | onView(withId(R.id.tabs)).check(matches(isDisplayed()))
70 | onView(withId(R.id.fab_favorite)).check(matches(isDisplayed()))
71 | onView(withId(R.id.fab_favorite)).perform(click())
72 | pressBack()
73 |
74 | onView(withId(R.id.favorite)).perform(click())
75 | onView(withId(R.id.rv_favorite)).check(matches(isDisplayed()))
76 | }
77 |
78 | @Test
79 | fun testDeleteUserFromFavorite() {
80 | onView(withId(R.id.favorite)).check(matches(isDisplayed()))
81 | onView(withId(R.id.favorite)).perform(click())
82 |
83 | onView(withId(R.id.rv_favorite)).perform(
84 | actionOnItemAtPosition(0, click())
85 | )
86 |
87 | onView(withId(R.id.user_detail_container)).check(matches(isDisplayed()))
88 | onView(withId(R.id.tabs)).check(matches(isDisplayed()))
89 | onView(withId(R.id.fab_favorite)).check(matches(isDisplayed()))
90 | onView(withId(R.id.fab_favorite)).perform(click())
91 | pressBack()
92 | }
93 |
94 | @Test
95 | fun testSearchUserFound() {
96 | onView(withId(R.id.search)).check(matches(isDisplayed()))
97 | onView(withId(R.id.search)).perform(click())
98 |
99 | onView(
100 | withId(androidx.appcompat.R.id.search_src_text)
101 | ).perform(
102 | clearText(), typeText("fikriyusrihan")
103 | ).perform(pressKey(KeyEvent.KEYCODE_ENTER))
104 |
105 | onView(withId(R.id.rv_users)).check(matches(isDisplayed()))
106 | onView(withId(R.id.rv_users)).perform(
107 | actionOnItemAtPosition(
108 | 0,
109 | click()
110 | )
111 | )
112 |
113 | onView(withId(R.id.user_detail_container)).check(matches(isDisplayed()))
114 | onView(withId(R.id.tabs)).check(matches(isDisplayed()))
115 | onView(withId(R.id.fab_favorite)).check(matches(isDisplayed()))
116 | }
117 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
18 |
21 |
24 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class MyApplication : Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/adapter/ListUserAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.artworkspace.github.data.remote.response.SimpleUser
7 | import com.artworkspace.github.utils.UIHelper.Companion.setImageGlide
8 | import com.artworkspace.github.databinding.UserCardBinding
9 |
10 | class ListUserAdapter(private val listUser: ArrayList) :
11 | RecyclerView.Adapter() {
12 |
13 | private lateinit var onItemClickCallback: OnItemClickCallback
14 |
15 | /**
16 | * Set an item click callback
17 | *
18 | * @param onItemClickCallback object that implements onItemClickCallback
19 | * @return Unit
20 | */
21 | fun setOnItemClickCallback(onItemClickCallback: OnItemClickCallback) {
22 | this.onItemClickCallback = onItemClickCallback
23 | }
24 |
25 | class ListViewHolder(var binding: UserCardBinding) : RecyclerView.ViewHolder(binding.root)
26 |
27 | override fun onCreateViewHolder(
28 | parent: ViewGroup,
29 | viewType: Int
30 | ): ListViewHolder {
31 | val binding = UserCardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
32 | return ListViewHolder(binding)
33 | }
34 |
35 | override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
36 | val user = listUser[position]
37 |
38 | holder.binding.apply {
39 | cardTvUsername.text = user.login
40 | cardImageProfile.setImageGlide(holder.itemView.context, user.avatarUrl)
41 | }
42 |
43 | holder.itemView.setOnClickListener { onItemClickCallback.onItemClicked(user) }
44 | }
45 |
46 | override fun getItemCount(): Int = listUser.size
47 |
48 | interface OnItemClickCallback {
49 | fun onItemClicked(user: SimpleUser)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/adapter/SectionPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.adapter
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.fragment.app.Fragment
6 | import androidx.viewpager2.adapter.FragmentStateAdapter
7 | import com.artworkspace.github.ui.view.FollowersFragment
8 | import com.artworkspace.github.ui.view.FollowingFragment
9 |
10 | class SectionPagerAdapter(activity: AppCompatActivity, private val username: String) :
11 | FragmentStateAdapter(activity) {
12 |
13 | override fun getItemCount(): Int = 2
14 |
15 | override fun createFragment(position: Int): Fragment {
16 | return when (position) {
17 | 0 -> {
18 | FollowersFragment().apply {
19 | arguments = Bundle().apply {
20 | putString(ARGS_USERNAME, username)
21 | }
22 | }
23 | }
24 | else -> {
25 | FollowingFragment().apply {
26 | arguments = Bundle().apply {
27 | putString(ARGS_USERNAME, username)
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
34 | companion object {
35 | const val ARGS_USERNAME = "username"
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/AppPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.booleanPreferencesKey
6 | import androidx.datastore.preferences.core.edit
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import javax.inject.Inject
10 |
11 | class AppPreferences @Inject constructor(private val dataStore: DataStore) {
12 | /**
13 | * Get theme setting for dark mode state from DataStore
14 | *
15 | * @return Flow
16 | */
17 | fun getThemeSetting(): Flow {
18 | return dataStore.data.map { preferences ->
19 | preferences[THEME_KEY] ?: false
20 | }
21 | }
22 |
23 | /**
24 | * Save theme setting for dark mode state to DataStore
25 | *
26 | * @param darkModeState Dark mode state to save
27 | */
28 | suspend fun saveThemeSetting(darkModeState: Boolean) {
29 | dataStore.edit { preferences ->
30 | preferences[THEME_KEY] = darkModeState
31 | }
32 | }
33 |
34 | companion object {
35 | private val THEME_KEY = booleanPreferencesKey("theme_setting")
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/Result.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data
2 |
3 | sealed class Result private constructor() {
4 | data class Success(val data: T) : Result()
5 | data class Error(val error: String) : Result()
6 | object Loading : Result()
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data
2 |
3 | import android.util.Log
4 | import com.artworkspace.github.BuildConfig
5 | import com.artworkspace.github.data.local.entity.UserEntity
6 | import com.artworkspace.github.data.local.room.UserDao
7 | import com.artworkspace.github.data.remote.response.SimpleUser
8 | import com.artworkspace.github.data.remote.response.User
9 | import com.artworkspace.github.data.remote.retrofit.ApiService
10 | import dagger.Module
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.components.ActivityRetainedComponent
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.flow
15 | import javax.inject.Inject
16 |
17 | class UserRepository @Inject constructor(
18 | private val apiService: ApiService,
19 | private val userDao: UserDao,
20 | private val preferences: AppPreferences
21 | ) {
22 |
23 | /**
24 | * Search GitHub user with API
25 | *
26 | * @param q GitHub username query
27 | * @return LiveData>>
28 | */
29 | fun searchUserByUsername(q: String): Flow>> = flow {
30 | emit(Result.Loading)
31 | try {
32 | val users = apiService.searchUsername(token = API_TOKEN, q).items
33 | emit(Result.Success(users))
34 | } catch (e: Exception) {
35 | Log.d(TAG, "searchUserByUsername: ${e.message.toString()}")
36 | emit(Result.Error(e.message.toString()))
37 | }
38 | }
39 |
40 | /**
41 | * Get following information of an user from API
42 | *
43 | * @param id GitHub username
44 | * @return Flow>>
45 | */
46 | fun getUserFollowing(id: String): Flow>> = flow {
47 | emit(Result.Loading)
48 | try {
49 | val users = apiService.getUserFollowing(token = API_TOKEN, id)
50 | emit(Result.Success(users))
51 | } catch (e: Exception) {
52 | Log.d(TAG, "getUserFollowing: ${e.message.toString()}")
53 | emit(Result.Error(e.message.toString()))
54 | }
55 | }
56 |
57 | /**
58 | * Get followers information of an user from API
59 | *
60 | * @param id GitHub username
61 | * @return Flow>>
62 | */
63 | fun getUserFollowers(id: String): Flow>> = flow {
64 | emit(Result.Loading)
65 | try {
66 | val users = apiService.getUserFollowers(token = API_TOKEN, id)
67 | emit(Result.Success(users))
68 | } catch (e: Exception) {
69 | Log.d(TAG, "getUserFollowers: ${e.message.toString()}")
70 | emit(Result.Error(e.message.toString()))
71 | }
72 | }
73 |
74 | /**
75 | * Get user detail information from API
76 | *
77 | * @param id GitHub username
78 | * @return LiveData>
79 | */
80 | fun getUserDetail(id: String): Flow> = flow {
81 | emit(Result.Loading)
82 | try {
83 | val user = apiService.getUserDetail(token = API_TOKEN, id)
84 | emit(Result.Success(user))
85 | } catch (e: Exception) {
86 | Log.d(TAG, "getUserDetail: ${e.message.toString()}")
87 | emit(Result.Error(e.message.toString()))
88 | }
89 | }
90 |
91 | /**
92 | * Determine this user is favorite or not
93 | *
94 | * @param id User id
95 | * @return Flow
96 | */
97 | fun isFavoriteUser(id: String): Flow = userDao.isFavoriteUser(id)
98 |
99 | /**
100 | * Get all favorite users from database
101 | *
102 | * @return LiveData>
103 | */
104 | fun getAllFavoriteUsers(): Flow> = userDao.getAllUsers()
105 |
106 | /**
107 | * Delete a favorite user from database
108 | *
109 | * @param user User to delete
110 | */
111 | suspend fun deleteFromFavorite(user: UserEntity) {
112 | userDao.delete(user)
113 | }
114 |
115 | /**
116 | * Save user as favorite to database
117 | *
118 | * @param user User to save
119 | */
120 | suspend fun saveUserAsFavorite(user: UserEntity) {
121 | userDao.insert(user)
122 | }
123 |
124 | /**
125 | * Get theme setting for dark mode state from DataStore
126 | *
127 | * @return Flow
128 | */
129 | fun getThemeSetting(): Flow = preferences.getThemeSetting()
130 |
131 | /**
132 | * Save theme setting for dark mode state to DataStore
133 | *
134 | * @param isDarkModeActive Dark mode state to save
135 | */
136 | suspend fun saveThemeSetting(isDarkModeActive: Boolean) {
137 | preferences.saveThemeSetting(isDarkModeActive)
138 | }
139 |
140 | companion object {
141 | private const val API_TOKEN = "Bearer ${BuildConfig.API_KEY}"
142 | private val TAG = UserRepository::class.java.simpleName
143 | }
144 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/local/entity/UserEntity.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data.local.entity
2 |
3 | import android.os.Parcelable
4 | import androidx.room.ColumnInfo
5 | import androidx.room.Entity
6 | import androidx.room.PrimaryKey
7 | import kotlinx.parcelize.Parcelize
8 |
9 | @Entity(tableName = "user")
10 | @Parcelize
11 | data class UserEntity(
12 | @PrimaryKey(autoGenerate = false)
13 | @ColumnInfo(name = "id")
14 | var id: String,
15 |
16 | @ColumnInfo(name = "avatar_url")
17 | var avatarUrl: String,
18 |
19 | @ColumnInfo(name = "is_favorite")
20 | var isFavorite: Boolean
21 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/local/room/UserDao.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data.local.room
2 |
3 | import androidx.room.*
4 | import com.artworkspace.github.data.local.entity.UserEntity
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | @Dao
8 | interface UserDao {
9 | @Insert(onConflict = OnConflictStrategy.IGNORE)
10 | suspend fun insert(userEntity: UserEntity)
11 |
12 | @Update
13 | suspend fun update(userEntity: UserEntity)
14 |
15 | @Delete
16 | suspend fun delete(userEntity: UserEntity)
17 |
18 | @Query("SELECT * FROM user ORDER BY id ASC")
19 | fun getAllUsers(): Flow>
20 |
21 | @Query("SELECT EXISTS(SELECT * FROM user WHERE id = :id AND is_favorite = 1)")
22 | fun isFavoriteUser(id: String): Flow
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/local/room/UserDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data.local.room
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.artworkspace.github.data.local.entity.UserEntity
6 |
7 | @Database(entities = [UserEntity::class], version = 1, exportSchema = false)
8 | abstract class UserDatabase : RoomDatabase() {
9 | abstract fun userDao(): UserDao
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/remote/response/ResponseGithub.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data.remote.response
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class ResponseSearch(
6 |
7 | @field:SerializedName("items")
8 | val items: ArrayList
9 | )
10 |
11 | data class User(
12 |
13 | @field:SerializedName("bio")
14 | val bio: String?,
15 |
16 | @field:SerializedName("login")
17 | val login: String,
18 |
19 | @field:SerializedName("blog")
20 | val blog: String?,
21 |
22 | @field:SerializedName("followers")
23 | val followers: Int,
24 |
25 | @field:SerializedName("avatar_url")
26 | val avatarUrl: String,
27 |
28 | @field:SerializedName("html_url")
29 | val htmlUrl: String,
30 |
31 | @field:SerializedName("following")
32 | val following: Int,
33 |
34 | @field:SerializedName("name")
35 | val name: String?,
36 |
37 | @field:SerializedName("company")
38 | val company: String?,
39 |
40 | @field:SerializedName("location")
41 | val location: String?,
42 |
43 | @field:SerializedName("public_repos")
44 | val publicRepos: Int,
45 | )
46 |
47 | data class SimpleUser(
48 |
49 | @field:SerializedName("avatar_url")
50 | val avatarUrl: String,
51 |
52 | @field:SerializedName("login")
53 | val login: String
54 | )
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/remote/retrofit/ApiConfig.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data.remote.retrofit
2 |
3 | import com.artworkspace.github.BuildConfig
4 | import okhttp3.OkHttpClient
5 | import okhttp3.logging.HttpLoggingInterceptor
6 | import retrofit2.Retrofit
7 | import retrofit2.converter.gson.GsonConverterFactory
8 |
9 | class ApiConfig {
10 | companion object {
11 | /**
12 | * ApiService provider
13 | *
14 | * @return ApiService
15 | */
16 | fun getApiService(): ApiService {
17 | val loggingInterceptor = if (BuildConfig.DEBUG) {
18 | HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
19 | } else {
20 | HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.NONE)
21 | }
22 |
23 | val client = OkHttpClient.Builder()
24 | .addInterceptor(loggingInterceptor)
25 | .build()
26 |
27 | val retrofit = Retrofit.Builder()
28 | .baseUrl("https://api.github.com/")
29 | .addConverterFactory(GsonConverterFactory.create())
30 | .client(client)
31 | .build()
32 |
33 | return retrofit.create(ApiService::class.java)
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/data/remote/retrofit/ApiService.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.data.remote.retrofit
2 |
3 | import com.artworkspace.github.data.remote.response.ResponseSearch
4 | import com.artworkspace.github.data.remote.response.SimpleUser
5 | import com.artworkspace.github.data.remote.response.User
6 | import retrofit2.http.GET
7 | import retrofit2.http.Header
8 | import retrofit2.http.Path
9 | import retrofit2.http.Query
10 |
11 | interface ApiService {
12 | /**
13 | * Search GitHub user with username
14 | *
15 | * @param token GitHub token auth
16 | * @param q Query
17 | * @return Call
18 | */
19 | @GET("search/users")
20 | suspend fun searchUsername(
21 | @Header("Authorization") token: String,
22 | @Query("q") q: String
23 | ): ResponseSearch
24 |
25 |
26 | /**
27 | * Get detail information of user by username
28 | *
29 | * @param token GitHub token auth
30 | * @param username Username
31 | * @return Call
32 | */
33 | @GET("users/{username}")
34 | suspend fun getUserDetail(
35 | @Header("Authorization") token: String,
36 | @Path("username") username: String
37 | ): User
38 |
39 |
40 | /**
41 | * Get followers information of an user
42 | *
43 | * @param token GitHub token auth
44 | * @param username Username
45 | * @return Call>
46 | */
47 | @GET("users/{username}/followers")
48 | suspend fun getUserFollowers(
49 | @Header("Authorization") token: String,
50 | @Path("username") username: String
51 | ): ArrayList
52 |
53 |
54 | /**
55 | * Get following information of an user
56 | *
57 | * @param token GitHub token auth
58 | * @param username Username
59 | * @return Call>
60 | */
61 | @GET("users/{username}/following")
62 | suspend fun getUserFollowing(
63 | @Header("Authorization") token: String,
64 | @Path("username") username: String
65 | ): ArrayList
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/di/ApiModule.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.di
2 |
3 | import com.artworkspace.github.data.remote.retrofit.ApiConfig
4 | import com.artworkspace.github.data.remote.retrofit.ApiService
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class ApiModule {
14 |
15 | @Provides
16 | @Singleton
17 | fun provideApiService(): ApiService = ApiConfig.getApiService()
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/di/DataStoreModule.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.di
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.preferencesDataStore
7 | import com.artworkspace.github.data.AppPreferences
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.android.qualifiers.ApplicationContext
12 | import dagger.hilt.components.SingletonComponent
13 | import javax.inject.Singleton
14 |
15 | private val Context.dataStore: DataStore by preferencesDataStore(name = "application")
16 |
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | class DataStoreModule {
20 |
21 | @Provides
22 | fun provideDataStore(@ApplicationContext context: Context): DataStore =
23 | context.dataStore
24 |
25 | @Provides
26 | @Singleton
27 | fun provideAppPreferences(dataStore: DataStore): AppPreferences =
28 | AppPreferences(dataStore)
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.artworkspace.github.data.local.room.UserDao
6 | import com.artworkspace.github.data.local.room.UserDatabase
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | class DatabaseModule {
17 |
18 | @Provides
19 | fun provideUserDao(userDatabase: UserDatabase): UserDao = userDatabase.userDao()
20 |
21 | @Provides
22 | @Singleton
23 | fun provideUserDatabase(@ApplicationContext context: Context): UserDatabase {
24 | return Room.databaseBuilder(
25 | context.applicationContext,
26 | UserDatabase::class.java,
27 | "note_database"
28 | ).build()
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/DetailUserActivity.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.Toast
8 | import androidx.activity.viewModels
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.lifecycle.Lifecycle
11 | import androidx.lifecycle.lifecycleScope
12 | import androidx.lifecycle.repeatOnLifecycle
13 | import androidx.viewpager2.widget.ViewPager2
14 | import com.artworkspace.github.R
15 | import com.artworkspace.github.adapter.SectionPagerAdapter
16 | import com.artworkspace.github.data.Result
17 | import com.artworkspace.github.data.local.entity.UserEntity
18 | import com.artworkspace.github.data.remote.response.User
19 | import com.artworkspace.github.databinding.ActivityDetailUserBinding
20 | import com.artworkspace.github.ui.viewmodel.DetailViewModel
21 | import com.artworkspace.github.utils.EspressoIdlingResource
22 | import com.artworkspace.github.utils.UIHelper.Companion.setAndVisible
23 | import com.artworkspace.github.utils.UIHelper.Companion.setImageGlide
24 | import com.google.android.material.tabs.TabLayout
25 | import com.google.android.material.tabs.TabLayoutMediator
26 | import dagger.hilt.android.AndroidEntryPoint
27 | import kotlinx.coroutines.flow.collect
28 | import kotlinx.coroutines.launch
29 |
30 | @AndroidEntryPoint
31 | class DetailUserActivity : AppCompatActivity(), View.OnClickListener {
32 |
33 | private var _binding: ActivityDetailUserBinding? = null
34 | private val binding get() = _binding!!
35 |
36 | private var username: String? = null
37 | private var profileUrl: String? = null
38 | private var userDetail: UserEntity? = null
39 | private var isFavorite: Boolean? = false
40 |
41 | private val detailViewModel: DetailViewModel by viewModels()
42 |
43 | override fun onCreate(savedInstanceState: Bundle?) {
44 | super.onCreate(savedInstanceState)
45 |
46 | _binding = ActivityDetailUserBinding.inflate(layoutInflater)
47 | username = intent.extras?.get(EXTRA_DETAIL) as String
48 |
49 | setContentView(binding.root)
50 | setViewPager()
51 | setToolbar(getString(R.string.profile))
52 |
53 | lifecycleScope.launch {
54 | repeatOnLifecycle(Lifecycle.State.STARTED) {
55 | launch {
56 | detailViewModel.userDetail.collect { result ->
57 | onDetailUserReceived(result)
58 | }
59 | }
60 | launch {
61 | detailViewModel.isFavoriteUser(username ?: "").collect { state ->
62 | isFavoriteUser(state)
63 | isFavorite = state
64 | }
65 | }
66 | launch {
67 | detailViewModel.isLoaded.collect { loaded ->
68 | if (!loaded) detailViewModel.getDetailUser(username ?: "")
69 | }
70 | }
71 | }
72 | }
73 |
74 | binding.btnOpen.setOnClickListener(this)
75 | binding.fabFavorite.setOnClickListener(this)
76 | }
77 |
78 | override fun onStart() {
79 | super.onStart()
80 | EspressoIdlingResource.increment()
81 | }
82 |
83 | override fun onSupportNavigateUp(): Boolean {
84 | onBackPressed()
85 | return true
86 | }
87 |
88 | override fun onClick(v: View?) {
89 | when (v?.id) {
90 | R.id.btn_open -> {
91 | Intent(Intent.ACTION_VIEW).apply {
92 | data = Uri.parse(profileUrl)
93 | }.also {
94 | startActivity(it)
95 | }
96 | }
97 | R.id.fab_favorite -> {
98 | if (isFavorite == true) {
99 | userDetail?.let { detailViewModel.deleteFromFavorite(it) }
100 | isFavoriteUser(false)
101 | Toast.makeText(this, "User deleted from favorite", Toast.LENGTH_SHORT).show()
102 | } else {
103 | userDetail?.let { detailViewModel.saveAsFavorite(it) }
104 | isFavoriteUser(true)
105 | Toast.makeText(this, "User added to favorite", Toast.LENGTH_SHORT).show()
106 | }
107 | }
108 | }
109 | }
110 |
111 | override fun onDestroy() {
112 | _binding = null
113 | username = null
114 | profileUrl = null
115 | isFavorite = null
116 | super.onDestroy()
117 | }
118 |
119 | /**
120 | * Parsing data to UI based on result
121 | *
122 | * @param result Result from API
123 | */
124 | private fun onDetailUserReceived(result: Result) {
125 | when (result) {
126 | is Result.Loading -> showLoading(true)
127 | is Result.Error -> {
128 | errorOccurred()
129 | showLoading(false)
130 | Toast.makeText(this, result.error, Toast.LENGTH_SHORT).show()
131 | }
132 | is Result.Success -> {
133 | result.data.let { user ->
134 | parseUserDetail(user)
135 |
136 | val userEntity = UserEntity(
137 | user.login,
138 | user.avatarUrl,
139 | true
140 | )
141 |
142 | userDetail = userEntity
143 | profileUrl = user.htmlUrl
144 | }
145 |
146 | showLoading(false)
147 | EspressoIdlingResource.decrement()
148 | }
149 | }
150 | }
151 |
152 | /**
153 | * Determine which icon to display in FAB
154 | *
155 | * @param favorite Is this favorite user?
156 | */
157 | private fun isFavoriteUser(favorite: Boolean) {
158 | if (favorite) {
159 | binding.fabFavorite.setImageResource(R.drawable.ic_baseline_favorite_24)
160 | } else {
161 | binding.fabFavorite.setImageResource(R.drawable.ic_baseline_favorite_border_24)
162 | }
163 | }
164 |
165 | /**
166 | * Setting UI when an error occurred
167 | *
168 | * @return Unit
169 | */
170 | private fun errorOccurred() {
171 | binding.apply {
172 | userDetailContainer.visibility = View.INVISIBLE
173 | tabs.visibility = View.INVISIBLE
174 | viewPager.visibility = View.INVISIBLE
175 | }
176 | }
177 |
178 | /**
179 | * Setting up toolbar
180 | *
181 | * @param title Toolbar title
182 | * @return Unit
183 | */
184 | private fun setToolbar(title: String) {
185 | setSupportActionBar(binding.toolbarDetail)
186 | binding.collapsingToolbar.isTitleEnabled = false
187 | supportActionBar?.apply {
188 | setDisplayShowHomeEnabled(true)
189 | setDisplayHomeAsUpEnabled(true)
190 | this.title = title
191 | }
192 | }
193 |
194 | /**
195 | * Setting up viewpager
196 | *
197 | * @return Unit
198 | */
199 | private fun setViewPager() {
200 | val viewPager: ViewPager2 = binding.viewPager
201 | val tabs: TabLayout = binding.tabs
202 |
203 | viewPager.adapter = SectionPagerAdapter(this, username!!)
204 |
205 | TabLayoutMediator(tabs, viewPager) { tab, position ->
206 | tab.text = resources.getString(TAB_TITLES[position])
207 | }.attach()
208 | }
209 |
210 | /**
211 | * Showing loading indicator
212 | *
213 | * @param isLoading Loading state
214 | * @return Unit
215 | */
216 | private fun showLoading(isLoading: Boolean) {
217 | if (isLoading) {
218 | binding.apply {
219 | pbLoading.visibility = View.VISIBLE
220 | appBarLayout.visibility = View.INVISIBLE
221 | viewPager.visibility = View.INVISIBLE
222 | fabFavorite.visibility = View.GONE
223 | }
224 | } else {
225 | binding.apply {
226 | pbLoading.visibility = View.GONE
227 | appBarLayout.visibility = View.VISIBLE
228 | viewPager.visibility = View.VISIBLE
229 | fabFavorite.visibility = View.VISIBLE
230 | }
231 | }
232 | }
233 |
234 | /**
235 | * Parsing User data to it's view
236 | *
237 | * @param user User dataclass
238 | * @return Unit
239 | */
240 | private fun parseUserDetail(user: User) {
241 | binding.apply {
242 | tvUsername.text = user.login
243 | tvRepositories.text = user.publicRepos.toString()
244 | tvFollowers.text = user.followers.toString()
245 | tvFollowing.text = user.following.toString()
246 |
247 | tvName.setAndVisible(user.name)
248 | tvBio.setAndVisible(user.bio)
249 | tvCompany.setAndVisible(user.company)
250 | tvLocation.setAndVisible(user.location)
251 | tvBlog.setAndVisible(user.blog)
252 |
253 | ivAvatar.setImageGlide(this@DetailUserActivity, user.avatarUrl)
254 | }
255 | }
256 |
257 | companion object {
258 | const val EXTRA_DETAIL = "extra_detail"
259 | private val TAB_TITLES = intArrayOf(
260 | R.string.followers,
261 | R.string.following
262 | )
263 | }
264 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/FavoriteActivity.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.lifecycle.lifecycleScope
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import com.artworkspace.github.R
11 | import com.artworkspace.github.adapter.ListUserAdapter
12 | import com.artworkspace.github.data.local.entity.UserEntity
13 | import com.artworkspace.github.data.remote.response.SimpleUser
14 | import com.artworkspace.github.databinding.ActivityFavoriteBinding
15 | import com.artworkspace.github.ui.viewmodel.FavoriteViewModel
16 | import com.artworkspace.github.utils.EspressoIdlingResource
17 | import dagger.hilt.android.AndroidEntryPoint
18 | import kotlinx.coroutines.flow.collect
19 | import kotlinx.coroutines.launch
20 |
21 | @AndroidEntryPoint
22 | class FavoriteActivity : AppCompatActivity() {
23 |
24 | private lateinit var binding: ActivityFavoriteBinding
25 | private val favoriteViewModel: FavoriteViewModel by viewModels()
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 |
30 | binding = ActivityFavoriteBinding.inflate(layoutInflater)
31 | setContentView(binding.root)
32 | setToolbar(getString(R.string.favorite))
33 |
34 | lifecycleScope.launchWhenStarted {
35 | launch {
36 | favoriteViewModel.favorite.collect {
37 | EspressoIdlingResource.increment()
38 | if (it.isNotEmpty()) showFavoriteUsers(it)
39 | else showMessage()
40 | }
41 | }
42 | }
43 | }
44 |
45 | override fun onSupportNavigateUp(): Boolean {
46 | onBackPressed()
47 | return true
48 | }
49 |
50 | private fun showMessage() {
51 | binding.tvMessage.visibility = View.VISIBLE
52 | binding.rvFavorite.visibility = View.GONE
53 |
54 | EspressoIdlingResource.decrement()
55 | }
56 |
57 | /**
58 | * Convert data type and display favorite users to the recycler view
59 | *
60 | * @param users List of favorite users
61 | * @return Unit
62 | */
63 | private fun showFavoriteUsers(users: List) {
64 | val listUsers = ArrayList()
65 |
66 | users.forEach { user ->
67 | val data = SimpleUser(
68 | user.avatarUrl,
69 | user.id
70 | )
71 |
72 | listUsers.add(data)
73 | }
74 |
75 | val listUserAdapter = ListUserAdapter(listUsers)
76 |
77 | binding.rvFavorite.apply {
78 | layoutManager = LinearLayoutManager(this@FavoriteActivity)
79 | adapter = listUserAdapter
80 | visibility = View.VISIBLE
81 | setHasFixedSize(true)
82 | }
83 |
84 | binding.tvMessage.visibility = View.GONE
85 |
86 | listUserAdapter.setOnItemClickCallback(object :
87 | ListUserAdapter.OnItemClickCallback {
88 | override fun onItemClicked(user: SimpleUser) {
89 | goToDetailUser(user)
90 | }
91 | })
92 |
93 | EspressoIdlingResource.decrement()
94 | }
95 |
96 | /**
97 | * Go to detail page with selected user data
98 | *
99 | * @param user Selected user
100 | * @return Unit
101 | */
102 | private fun goToDetailUser(user: SimpleUser) {
103 | Intent(this@FavoriteActivity, DetailUserActivity::class.java).apply {
104 | putExtra(DetailUserActivity.EXTRA_DETAIL, user.login)
105 | }.also {
106 | startActivity(it)
107 | }
108 | }
109 |
110 | /**
111 | * Setting up toolbar
112 | *
113 | * @param title Toolbar title
114 | * @return Unit
115 | */
116 | private fun setToolbar(title: String) {
117 | setSupportActionBar(binding.toolbar)
118 | supportActionBar?.apply {
119 | setDisplayShowHomeEnabled(true)
120 | setDisplayHomeAsUpEnabled(true)
121 | this.title = title
122 | }
123 | }
124 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/FollowersFragment.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.lifecycle.lifecycleScope
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import com.artworkspace.github.adapter.ListUserAdapter
13 | import com.artworkspace.github.adapter.SectionPagerAdapter.Companion.ARGS_USERNAME
14 | import com.artworkspace.github.data.Result
15 | import com.artworkspace.github.data.remote.response.SimpleUser
16 | import com.artworkspace.github.databinding.FragmentFollowersBinding
17 | import com.artworkspace.github.ui.viewmodel.FollowersViewModel
18 | import dagger.hilt.android.AndroidEntryPoint
19 | import kotlinx.coroutines.flow.collect
20 | import kotlinx.coroutines.launch
21 |
22 | @AndroidEntryPoint
23 | class FollowersFragment : Fragment() {
24 |
25 | private var _binding: FragmentFollowersBinding? = null
26 | private val binding get() = _binding!!
27 |
28 | private val followersViewModel: FollowersViewModel by viewModels()
29 |
30 | override fun onCreateView(
31 | inflater: LayoutInflater, container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View {
34 | _binding = FragmentFollowersBinding.inflate(layoutInflater, container, false)
35 | return binding.root
36 | }
37 |
38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
39 | super.onViewCreated(view, savedInstanceState)
40 |
41 | val username = arguments?.getString(ARGS_USERNAME) ?: ""
42 | viewLifecycleOwner.lifecycleScope.launchWhenStarted {
43 | launch {
44 | followersViewModel.followers.collect { result ->
45 | onFollowersResultReceived(result)
46 | }
47 | }
48 | launch {
49 | followersViewModel.isLoaded.collect { loaded ->
50 | if (!loaded) followersViewModel.getUserFollowers(username)
51 | }
52 | }
53 | }
54 | }
55 |
56 | override fun onDestroy() {
57 | _binding = null
58 | super.onDestroy()
59 | }
60 |
61 | /**
62 | * Parsing data to UI based on result
63 | *
64 | * @param result Result from API
65 | */
66 | private fun onFollowersResultReceived(result: Result>) {
67 | when (result) {
68 | is Result.Loading -> showLoading(true)
69 | is Result.Error -> {
70 | showLoading(false)
71 | }
72 | is Result.Success -> {
73 | showFollowers(result.data)
74 | showLoading(false)
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * Showing up result, setup layout manager, adapter, and onClickItemCallback
81 | *
82 | * @param users Followers
83 | * @return Unit
84 | */
85 | private fun showFollowers(users: ArrayList) {
86 | if (users.size > 0) {
87 | val linearLayoutManager = LinearLayoutManager(activity)
88 | val listAdapter = ListUserAdapter(users)
89 |
90 | binding.rvUsers.apply {
91 | layoutManager = linearLayoutManager
92 | adapter = listAdapter
93 | setHasFixedSize(true)
94 | }
95 |
96 | listAdapter.setOnItemClickCallback(object :
97 | ListUserAdapter.OnItemClickCallback {
98 | override fun onItemClicked(user: SimpleUser) {
99 | goToDetailUser(user)
100 | }
101 |
102 | })
103 | } else binding.tvStatus.visibility = View.VISIBLE
104 | }
105 |
106 | /**
107 | * Showing loading indicator
108 | *
109 | * @param isLoading Loading state
110 | * @return Unit
111 | */
112 | private fun showLoading(isLoading: Boolean) {
113 | if (isLoading) binding.pbLoading.visibility = View.VISIBLE
114 | else binding.pbLoading.visibility = View.GONE
115 | }
116 |
117 | /**
118 | * Go to detail page with selected user data
119 | *
120 | * @param user Selected user
121 | * @return Unit
122 | */
123 | private fun goToDetailUser(user: SimpleUser) {
124 | Intent(activity, DetailUserActivity::class.java).apply {
125 | putExtra(DetailUserActivity.EXTRA_DETAIL, user.login)
126 | }.also {
127 | startActivity(it)
128 | }
129 | }
130 |
131 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/FollowingFragment.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.lifecycle.lifecycleScope
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import com.artworkspace.github.adapter.ListUserAdapter
13 | import com.artworkspace.github.adapter.SectionPagerAdapter.Companion.ARGS_USERNAME
14 | import com.artworkspace.github.data.Result
15 | import com.artworkspace.github.data.remote.response.SimpleUser
16 | import com.artworkspace.github.databinding.FragmentFollowingBinding
17 | import com.artworkspace.github.ui.viewmodel.FollowingViewModel
18 | import dagger.hilt.android.AndroidEntryPoint
19 | import kotlinx.coroutines.flow.collect
20 | import kotlinx.coroutines.launch
21 |
22 | @AndroidEntryPoint
23 | class FollowingFragment : Fragment() {
24 |
25 | private var _binding: FragmentFollowingBinding? = null
26 | private val binding get() = _binding!!
27 |
28 | private val followingViewModel: FollowingViewModel by viewModels()
29 |
30 | override fun onCreateView(
31 | inflater: LayoutInflater, container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View {
34 | _binding = FragmentFollowingBinding.inflate(layoutInflater, container, false)
35 | return binding.root
36 | }
37 |
38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
39 | super.onViewCreated(view, savedInstanceState)
40 |
41 | val username = arguments?.getString(ARGS_USERNAME) ?: ""
42 | viewLifecycleOwner.lifecycleScope.launchWhenStarted {
43 | launch {
44 | followingViewModel.following.collect {
45 | onFollowingResultReceived(it)
46 | }
47 | }
48 | launch {
49 | followingViewModel.isLoaded.collect { loaded ->
50 | if (!loaded) followingViewModel.getUserFollowing(username)
51 | }
52 | }
53 | }
54 | }
55 |
56 | override fun onDestroy() {
57 | _binding = null
58 | super.onDestroy()
59 | }
60 |
61 | /**
62 | * Parsing data to UI based on result
63 | *
64 | * @param result Result from API
65 | */
66 | private fun onFollowingResultReceived(result: Result>) {
67 | when (result) {
68 | is Result.Loading -> showLoading(true)
69 | is Result.Error -> {
70 | showLoading(false)
71 | }
72 | is Result.Success -> {
73 | showFollowing(result.data)
74 | showLoading(false)
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * Showing up result, setup layout manager, adapter, and onClickItemCallback
81 | *
82 | * @param users Following
83 | * @return Unit
84 | */
85 | private fun showFollowing(users: ArrayList) {
86 | if (users.size > 0) {
87 | val linearLayoutManager = LinearLayoutManager(activity)
88 | val listAdapter = ListUserAdapter(users)
89 |
90 | binding.rvUsers.apply {
91 | layoutManager = linearLayoutManager
92 | adapter = listAdapter
93 | setHasFixedSize(true)
94 | }
95 |
96 | listAdapter.setOnItemClickCallback(object :
97 | ListUserAdapter.OnItemClickCallback {
98 | override fun onItemClicked(user: SimpleUser) {
99 | goToDetailUser(user)
100 | }
101 |
102 | })
103 | } else binding.tvStatus.visibility = View.VISIBLE
104 | }
105 |
106 | /**
107 | * Showing loading indicator
108 | *
109 | * @param isLoading Loading state
110 | * @return Unit
111 | */
112 | private fun showLoading(isLoading: Boolean) {
113 | if (isLoading) binding.pbLoading.visibility = View.VISIBLE
114 | else binding.pbLoading.visibility = View.GONE
115 | }
116 |
117 | /**
118 | * Go to detail page with selected user data
119 | *
120 | * @param user Selected user
121 | * @return Unit
122 | */
123 | private fun goToDetailUser(user: SimpleUser) {
124 | Intent(activity, DetailUserActivity::class.java).apply {
125 | putExtra(DetailUserActivity.EXTRA_DETAIL, user.login)
126 | }.also {
127 | startActivity(it)
128 | }
129 | }
130 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.app.SearchManager
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.view.Menu
8 | import android.view.MenuItem
9 | import android.view.View
10 | import android.widget.Toast
11 | import androidx.activity.viewModels
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.appcompat.app.AppCompatDelegate
14 | import androidx.appcompat.widget.SearchView
15 | import androidx.lifecycle.Lifecycle
16 | import androidx.lifecycle.lifecycleScope
17 | import androidx.lifecycle.repeatOnLifecycle
18 | import androidx.recyclerview.widget.LinearLayoutManager
19 | import com.artworkspace.github.R
20 | import com.artworkspace.github.adapter.ListUserAdapter
21 | import com.artworkspace.github.data.Result
22 | import com.artworkspace.github.data.remote.response.SimpleUser
23 | import com.artworkspace.github.databinding.ActivityMainBinding
24 | import com.artworkspace.github.ui.view.DetailUserActivity.Companion.EXTRA_DETAIL
25 | import com.artworkspace.github.ui.viewmodel.MainViewModel
26 | import com.artworkspace.github.utils.EspressoIdlingResource
27 | import dagger.hilt.android.AndroidEntryPoint
28 | import kotlinx.coroutines.flow.collect
29 | import kotlinx.coroutines.launch
30 |
31 | @AndroidEntryPoint
32 | class MainActivity : AppCompatActivity() {
33 |
34 | private var _binding: ActivityMainBinding? = null
35 | private val binding get() = _binding!!
36 |
37 | private val mainViewModel: MainViewModel by viewModels()
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 |
42 | _binding = ActivityMainBinding.inflate(layoutInflater)
43 | setContentView(binding.root)
44 |
45 | setSupportActionBar(binding.toolbarHome)
46 | supportActionBar?.setDisplayShowTitleEnabled(false)
47 |
48 | lifecycleScope.launch {
49 | repeatOnLifecycle(Lifecycle.State.STARTED) {
50 | launch {
51 | mainViewModel.themeSetting.collect { state ->
52 | if (state) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
53 | else AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
54 | }
55 | }
56 | launch {
57 | mainViewModel.users.collect { result ->
58 | showSearchingResult(result)
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 | override fun onStart() {
66 | super.onStart()
67 | EspressoIdlingResource.increment()
68 | }
69 |
70 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
71 | val inflater = menuInflater
72 | inflater.inflate(R.menu.home_menu, menu)
73 |
74 | val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
75 | val searchView = menu.findItem(R.id.search).actionView as SearchView
76 |
77 | searchView.apply {
78 | setSearchableInfo(searchManager.getSearchableInfo(componentName))
79 | queryHint = getString(R.string.github_username)
80 | setOnQueryTextListener(object : SearchView.OnQueryTextListener {
81 | override fun onQueryTextSubmit(query: String?): Boolean {
82 | EspressoIdlingResource.increment()
83 | mainViewModel.searchUserByUsername(query ?: "")
84 | clearFocus()
85 | return true
86 | }
87 |
88 | override fun onQueryTextChange(newText: String?): Boolean {
89 | return false
90 | }
91 |
92 | })
93 | }
94 | return true
95 | }
96 |
97 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
98 | when (item.itemId) {
99 | R.id.favorite -> {
100 | Intent(this@MainActivity, FavoriteActivity::class.java).also {
101 | startActivity(it)
102 | }
103 | }
104 | R.id.setting -> {
105 | Intent(this@MainActivity, SettingActivity::class.java).also {
106 | startActivity(it)
107 | }
108 | }
109 | }
110 | return super.onOptionsItemSelected(item)
111 | }
112 |
113 | override fun onDestroy() {
114 | _binding = null
115 | super.onDestroy()
116 | }
117 |
118 | /**
119 | * Setting UI when an error occurred
120 | *
121 | * @return Unit
122 | */
123 | private fun errorOccurred() {
124 | Toast.makeText(this@MainActivity, "An Error is Occurred", Toast.LENGTH_SHORT).show()
125 | }
126 |
127 | /**
128 | * Determine loading indicator is visible or not
129 | *
130 | * @param isLoading Loading state
131 | * @return Unit
132 | */
133 | private fun showLoading(isLoading: Boolean) {
134 | if (isLoading) {
135 | binding.pbLoading.visibility = View.VISIBLE
136 | binding.rvUsers.visibility = View.GONE
137 | } else {
138 | binding.pbLoading.visibility = View.GONE
139 | binding.rvUsers.visibility = View.VISIBLE
140 | }
141 | }
142 |
143 | /**
144 | * Showing up result, setup layout manager, adapter, and onClickItemCallback
145 | *
146 | * @param result Result from viewmodel
147 | * @return Unit
148 | */
149 | private fun showSearchingResult(result: Result>) {
150 | when (result) {
151 | is Result.Loading -> showLoading(true)
152 | is Result.Error -> {
153 | errorOccurred()
154 | showLoading(false)
155 | }
156 | is Result.Success -> {
157 | binding.tvResultCount.text = getString(R.string.showing_results, result.data.size)
158 | val listUserAdapter = ListUserAdapter(result.data)
159 |
160 | binding.rvUsers.apply {
161 | layoutManager = LinearLayoutManager(this@MainActivity)
162 | adapter = listUserAdapter
163 | setHasFixedSize(true)
164 | }
165 |
166 | listUserAdapter.setOnItemClickCallback(object :
167 | ListUserAdapter.OnItemClickCallback {
168 | override fun onItemClicked(user: SimpleUser) {
169 | goToDetailUser(user)
170 | }
171 |
172 | })
173 | showLoading(false)
174 | EspressoIdlingResource.decrement()
175 | }
176 | }
177 | }
178 |
179 | /**
180 | * Go to detail page with selected user data
181 | *
182 | * @param user Selected user
183 | * @return Unit
184 | */
185 | private fun goToDetailUser(user: SimpleUser) {
186 | Intent(this@MainActivity, DetailUserActivity::class.java).apply {
187 | putExtra(EXTRA_DETAIL, user.login)
188 | }.also {
189 | startActivity(it)
190 | }
191 | }
192 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/SettingActivity.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.os.Bundle
4 | import android.widget.CompoundButton
5 | import androidx.activity.viewModels
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.appcompat.app.AppCompatDelegate
8 | import androidx.lifecycle.Lifecycle
9 | import androidx.lifecycle.lifecycleScope
10 | import androidx.lifecycle.repeatOnLifecycle
11 | import com.artworkspace.github.R
12 | import com.artworkspace.github.databinding.ActivitySettingBinding
13 | import com.artworkspace.github.ui.viewmodel.SettingViewModel
14 | import dagger.hilt.android.AndroidEntryPoint
15 | import kotlinx.coroutines.flow.collect
16 | import kotlinx.coroutines.launch
17 |
18 | @AndroidEntryPoint
19 | class SettingActivity : AppCompatActivity(), CompoundButton.OnCheckedChangeListener {
20 |
21 | private var _binding: ActivitySettingBinding? = null
22 | private val binding get() = _binding!!
23 |
24 | private val settingViewModel: SettingViewModel by viewModels ()
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 |
29 | _binding = ActivitySettingBinding.inflate(layoutInflater)
30 | setContentView(binding.root)
31 | setToolbar(getString(R.string.setting))
32 |
33 | lifecycleScope.launch {
34 | repeatOnLifecycle(Lifecycle.State.STARTED) {
35 | launch {
36 | settingViewModel.getThemeSetting.collect { state ->
37 | if (state) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
38 | else AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
39 |
40 | binding.switchDarkMode.isChecked = state
41 | }
42 | }
43 | }
44 | }
45 |
46 | binding.switchDarkMode.setOnCheckedChangeListener(this)
47 | }
48 |
49 | override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
50 | when (buttonView?.id) {
51 | R.id.switch_dark_mode -> settingViewModel.saveThemeSetting(isChecked)
52 | }
53 | }
54 |
55 | override fun onSupportNavigateUp(): Boolean {
56 | onBackPressed()
57 | return true
58 | }
59 |
60 | override fun onDestroy() {
61 | _binding = null
62 | super.onDestroy()
63 | }
64 |
65 | /**
66 | * Setting up toolbar
67 | *
68 | * @param title Toolbar title
69 | * @return Unit
70 | */
71 | private fun setToolbar(title: String) {
72 | setSupportActionBar(binding.toolbarSetting)
73 | supportActionBar?.apply {
74 | setDisplayShowHomeEnabled(true)
75 | setDisplayHomeAsUpEnabled(true)
76 | this.title = title
77 | }
78 | }
79 |
80 |
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/view/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.view
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.runBlocking
9 |
10 | @SuppressLint("CustomSplashScreen")
11 | class SplashActivity : AppCompatActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 |
15 | Intent(this, MainActivity::class.java).also {
16 | runBlocking {
17 | delay(500)
18 | startActivity(it)
19 | finish()
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/viewmodel/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.artworkspace.github.data.Result
6 | import com.artworkspace.github.data.UserRepository
7 | import com.artworkspace.github.data.local.entity.UserEntity
8 | import com.artworkspace.github.data.remote.response.User
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.flow.collect
14 | import kotlinx.coroutines.launch
15 | import javax.inject.Inject
16 |
17 | @HiltViewModel
18 | class DetailViewModel @Inject constructor(private val repository: UserRepository) : ViewModel() {
19 |
20 | private val _userDetail = MutableStateFlow>(Result.Loading)
21 | val userDetail = _userDetail.asStateFlow()
22 |
23 | private val _isLoaded = MutableStateFlow(false)
24 | val isLoaded = _isLoaded.asStateFlow()
25 |
26 | /**
27 | * Get user detail information
28 | *
29 | * @param username GitHub username
30 | * @return Unit
31 | */
32 | fun getDetailUser(username: String) {
33 | _userDetail.value = Result.Loading
34 | viewModelScope.launch {
35 | repository.getUserDetail(username).collect {
36 | _userDetail.value = it
37 | }
38 | }
39 |
40 | _isLoaded.value = true
41 | }
42 |
43 | /**
44 | * Save user to database as favorite user
45 | *
46 | * @param user New favorite user
47 | */
48 | fun saveAsFavorite(user: UserEntity) {
49 | viewModelScope.launch {
50 | repository.saveUserAsFavorite(user)
51 | }
52 | }
53 |
54 | /**
55 | * Delete favorite user from database
56 | *
57 | * @param user User to delete
58 | */
59 | fun deleteFromFavorite(user: UserEntity) {
60 | viewModelScope.launch {
61 | repository.deleteFromFavorite(user)
62 | }
63 | }
64 |
65 | /**
66 | * Determine this is favorite user or not
67 | *
68 | * @param id User id
69 | * @return LiveData
70 | */
71 | fun isFavoriteUser(id: String): Flow = repository.isFavoriteUser(id)
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/viewmodel/FavoriteViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.artworkspace.github.data.UserRepository
6 | import com.artworkspace.github.data.local.entity.UserEntity
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.asStateFlow
10 | import kotlinx.coroutines.flow.collect
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class FavoriteViewModel @Inject constructor(private val repository: UserRepository) : ViewModel() {
16 |
17 | private val _favorites = MutableStateFlow(listOf())
18 | val favorite = _favorites.asStateFlow()
19 |
20 | init {
21 | getFavoriteUsers()
22 | }
23 |
24 | /**
25 | * Get all favorite users from database
26 | *
27 | * @return LiveData>
28 | */
29 | private fun getFavoriteUsers() {
30 | viewModelScope.launch {
31 | repository.getAllFavoriteUsers().collect {
32 | _favorites.value = it
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/viewmodel/FollowersViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.artworkspace.github.data.Result
6 | import com.artworkspace.github.data.UserRepository
7 | import com.artworkspace.github.data.remote.response.SimpleUser
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import kotlinx.coroutines.flow.collect
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class FollowersViewModel @Inject constructor(private val repository: UserRepository) : ViewModel() {
17 |
18 | private val _isLoaded = MutableStateFlow(false)
19 | val isLoaded = _isLoaded.asStateFlow()
20 |
21 | private val _followers = MutableStateFlow>>(Result.Loading)
22 | val followers = _followers.asStateFlow()
23 |
24 | /**
25 | * Get followers information of an user
26 | *
27 | * @param username GitHub username
28 | * @return Unit
29 | */
30 | fun getUserFollowers(username: String) {
31 | _followers.value = Result.Loading
32 | viewModelScope.launch {
33 | repository.getUserFollowers(username).collect {
34 | _followers.value = it
35 | }
36 | }
37 |
38 | _isLoaded.value = true
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/viewmodel/FollowingViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.artworkspace.github.data.Result
6 | import com.artworkspace.github.data.UserRepository
7 | import com.artworkspace.github.data.remote.response.SimpleUser
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import kotlinx.coroutines.flow.catch
12 | import kotlinx.coroutines.flow.collect
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class FollowingViewModel @Inject constructor(private val repository: UserRepository) : ViewModel() {
18 |
19 | private val _isLoaded = MutableStateFlow(false)
20 | val isLoaded = _isLoaded.asStateFlow()
21 |
22 | private val _following = MutableStateFlow>>(Result.Loading)
23 | val following = _following.asStateFlow()
24 |
25 | /**
26 | * Get following information of an user
27 | *
28 | * @param username GitHub username
29 | * @return Unit
30 | */
31 | fun getUserFollowing(username: String) {
32 | _following.value = Result.Loading
33 | viewModelScope.launch {
34 | repository.getUserFollowing(username).catch { e ->
35 | _following.value = Result.Error(e.message.toString())
36 | }.collect {
37 | _following.value = it
38 | }
39 | }
40 |
41 | _isLoaded.value = true
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/viewmodel/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.artworkspace.github.data.Result
6 | import com.artworkspace.github.data.UserRepository
7 | import com.artworkspace.github.data.remote.response.SimpleUser
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.flow.collect
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class MainViewModel @Inject constructor(private val repository: UserRepository) : ViewModel() {
18 |
19 | val themeSetting: Flow = repository.getThemeSetting()
20 |
21 | private val _users = MutableStateFlow>>(Result.Loading)
22 | val users = _users.asStateFlow()
23 |
24 | init {
25 | searchUserByUsername("\"\"")
26 | }
27 |
28 | /**
29 | * Search GitHub user
30 | *
31 | * @param query GitHub username
32 | * @return LiveData
33 | */
34 | fun searchUserByUsername(query: String) {
35 | _users.value = Result.Loading
36 | viewModelScope.launch {
37 | repository.searchUserByUsername(query).collect {
38 | _users.value = it
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/ui/viewmodel/SettingViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.artworkspace.github.data.UserRepository
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.launch
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class SettingViewModel @Inject constructor(private val userRepository: UserRepository) :
13 | ViewModel() {
14 |
15 | /**
16 | * Get theme setting from DataStore
17 | *
18 | * @return LiveData
19 | */
20 | val getThemeSetting: Flow = userRepository.getThemeSetting()
21 |
22 | /**
23 | * Saving dark mode state to DataStore
24 | *
25 | * @param darkModeState Dark mode state
26 | */
27 | fun saveThemeSetting(darkModeState: Boolean) {
28 | viewModelScope.launch {
29 | userRepository.saveThemeSetting(darkModeState)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/utils/EspressoIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.utils
2 |
3 | import androidx.test.espresso.IdlingResource
4 | import androidx.test.espresso.idling.CountingIdlingResource
5 |
6 |
7 | object EspressoIdlingResource {
8 | private val countingIdlingResource = CountingIdlingResource("GLOBAL")
9 |
10 | val idlingResource: IdlingResource
11 | get() = countingIdlingResource
12 |
13 | fun increment() = countingIdlingResource.increment()
14 |
15 | fun decrement() = countingIdlingResource.decrement()
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/artworkspace/github/utils/UIHelper.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github.utils
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.widget.TextView
6 | import com.artworkspace.github.R
7 | import com.bumptech.glide.Glide
8 | import de.hdodenhof.circleimageview.CircleImageView
9 |
10 | class UIHelper {
11 | companion object {
12 | /**
13 | * Extension function to set CircleImageView using Glide
14 | *
15 | * @param context Context
16 | * @param url Image URL
17 | * @return Unit
18 | */
19 | fun CircleImageView.setImageGlide(context: Context, url: String) {
20 | Glide
21 | .with(context)
22 | .load(url)
23 | .placeholder(R.drawable.profile_placeholder)
24 | .into(this)
25 | }
26 |
27 | /**
28 | * Extension function to set and show hidden TextView if the text has value
29 | *
30 | * @param text Text to set
31 | * @return Unit
32 | */
33 | fun TextView.setAndVisible(text: String?) {
34 | if (!text.isNullOrBlank()) {
35 | this.text = text
36 | this.visibility = View.VISIBLE
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/profile_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/drawable-v24/profile_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dummy_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/drawable/dummy_profile.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github_logo_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/drawable/github_logo_black.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github_logo_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/drawable/github_logo_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_apartment_16.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_blog_16.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_favorite_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_favorite_border_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_location_on_16.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_person_16.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_search_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github_mark.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github_mark_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_bg_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_bg_light.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_detail_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
22 |
23 |
29 |
30 |
36 |
37 |
42 |
43 |
49 |
50 |
57 |
58 |
64 |
65 |
66 |
67 |
68 |
69 |
78 |
79 |
91 |
92 |
97 |
98 |
103 |
104 |
109 |
110 |
111 |
112 |
117 |
118 |
123 |
124 |
129 |
130 |
131 |
132 |
137 |
138 |
143 |
144 |
149 |
150 |
151 |
152 |
153 |
154 |
162 |
163 |
173 |
174 |
185 |
186 |
198 |
199 |
211 |
212 |
224 |
225 |
226 |
227 |
237 |
238 |
239 |
240 |
248 |
249 |
250 |
251 |
259 |
260 |
261 |
262 |
267 |
268 |
272 |
273 |
274 |
275 |
281 |
282 |
291 |
292 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_favorite.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
28 |
29 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
27 |
28 |
33 |
34 |
35 |
36 |
45 |
46 |
47 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_setting.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
27 |
28 |
34 |
35 |
40 |
41 |
47 |
48 |
52 |
53 |
54 |
55 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_followers.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
23 |
24 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_following.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
23 |
24 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/user_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/home_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
25 |
26 |
34 |
35 |
39 |
40 |
44 |
45 |
52 |
53 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #FFFFFFFF
5 | #161B22
6 | #0D1013
7 | #221d16
8 | #e8e8e9
9 | #bababa
10 | #e9e9e8
11 | #DCDCDB
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | GitHub
3 | User Name
4 | Name
5 | Company
6 | GitHub Logo
7 | 24
8 | Repositories
9 | Following
10 | Followers
11 | Location
12 | Open on GitHub
13 | Profile
14 | Search
15 | GitHub Username
16 | Showing %d results
17 | This is my bio
18 | mysite.com
19 | No users found.
20 | User Profile Picture
21 | Setting
22 | Favorite
23 | Theme
24 | Dark Mode
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
24 |
25 |
33 |
34 |
38 |
39 |
43 |
44 |
51 |
52 |
56 |
--------------------------------------------------------------------------------
/app/src/test/java/com/artworkspace/github/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.artworkspace.github
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | mavenCentral()
5 | }
6 | dependencies {
7 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.41'
8 | }
9 | }
10 |
11 | plugins {
12 | id 'com.android.application' version '7.1.2' apply false
13 | id 'com.android.library' version '7.1.2' apply false
14 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
15 | }
16 |
17 | task clean(type: Delete) {
18 | delete rootProject.buildDir
19 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fikriyusrihan/dicoding-android-fundamental-submission/3548de13ebdac8671b1cd96af3328f33d2aa2a28/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Feb 22 13:25:30 ICT 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "GitHub"
16 | include ':app'
17 |
--------------------------------------------------------------------------------