diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3c7772a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aadc7aa --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Search My Profile in Github + +## branch + +### hunki - like develop + +### feature_hunki : using to make feat \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 9f9e574..c64e4ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-kapt' } android { @@ -30,16 +31,48 @@ android { kotlinOptions { jvmTarget = '1.8' } + dataBinding{ + enabled = true + } + kotlinOptions{ + jvmTarget = "1.8" + } } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + // retrofit + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + + // coroutine + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0' + + // liveData + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' + + // viewModel + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$version_lifecycle" + + // ktx + implementation "androidx.core:core-ktx:1.3.2" + implementation "androidx.fragment:fragment-ktx:1.2.5" + implementation "androidx.activity:activity-ktx:1.1.0" + + // Room database + implementation "androidx.room:room-runtime:$version_room" + kapt "androidx.room:room-compiler:$version_room" + + // Kotlin Extensions and Coroutines support for Room + implementation "androidx.room:room-ktx:$version_room" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e9c8904..0f089cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,14 +2,16 @@ + + + - + diff --git a/app/src/main/java/com/siba/searchmvvmpractice/MainActivity.kt b/app/src/main/java/com/siba/searchmvvmpractice/MainActivity.kt deleted file mode 100644 index c5fd61e..0000000 --- a/app/src/main/java/com/siba/searchmvvmpractice/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.siba.searchmvvmpractice - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/injection/Injection.kt b/app/src/main/java/com/siba/searchmvvmpractice/injection/Injection.kt new file mode 100644 index 0000000..a30e49b --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/injection/Injection.kt @@ -0,0 +1,26 @@ +package com.siba.searchmvvmpractice.injection + +import android.content.Context +import androidx.lifecycle.ViewModelProvider +import com.siba.searchmvvmpractice.local.database.SearchTermDatabase +import com.siba.searchmvvmpractice.remote.RetrofitService +import com.siba.searchmvvmpractice.remote.api.RetrofitBuilder +import com.siba.searchmvvmpractice.repository.SearchRepository +import com.siba.searchmvvmpractice.ui.base.SearchViewModelFactory + +object Injection { + + private fun provideRetrofitService(): RetrofitService { + return RetrofitBuilder.retrofitService + } + + private fun provideMainRepository(context: Context): SearchRepository { + val database = SearchTermDatabase.getInstance(context) + return SearchRepository(provideRetrofitService(), database.searchTermDao) + } + + fun provideSearchViewModelFactory(context: Context): ViewModelProvider.Factory { + return SearchViewModelFactory(provideMainRepository(context)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/local/dao/SearchTermDao.kt b/app/src/main/java/com/siba/searchmvvmpractice/local/dao/SearchTermDao.kt new file mode 100644 index 0000000..0e86ef1 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/local/dao/SearchTermDao.kt @@ -0,0 +1,20 @@ +package com.siba.searchmvvmpractice.local.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.siba.searchmvvmpractice.local.entity.RecentSearchTerm + +@Dao +interface SearchTermDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertKeyword(recentSearchTerm: RecentSearchTerm) + + @Query("DELETE FROM recent_search_term_table") + fun clear() + + @Query("SELECT * FROM recent_search_term_table") + fun getAllKeyword(): LiveData> +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/local/database/SearchTermDatabase.kt b/app/src/main/java/com/siba/searchmvvmpractice/local/database/SearchTermDatabase.kt new file mode 100644 index 0000000..f7db529 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/local/database/SearchTermDatabase.kt @@ -0,0 +1,34 @@ +package com.siba.searchmvvmpractice.local.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.siba.searchmvvmpractice.local.dao.SearchTermDao +import com.siba.searchmvvmpractice.local.entity.RecentSearchTerm + +@Database(entities = [RecentSearchTerm::class], version = 1) +abstract class SearchTermDatabase : RoomDatabase() { + abstract val searchTermDao: SearchTermDao + + companion object { + @Volatile + private var INSTANCE: SearchTermDatabase? = null + + fun getInstance(context: Context): SearchTermDatabase { + synchronized(this) { + var instance = INSTANCE + + if (instance == null) { + instance = Room.databaseBuilder( + context.applicationContext, + SearchTermDatabase::class.java, + "search_keyword_history_database2" + ).build() + INSTANCE = instance + } + return instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/local/entity/RecentSearchTerm.kt b/app/src/main/java/com/siba/searchmvvmpractice/local/entity/RecentSearchTerm.kt new file mode 100644 index 0000000..5adb0b7 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/local/entity/RecentSearchTerm.kt @@ -0,0 +1,12 @@ +package com.siba.searchmvvmpractice.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "recent_search_term_table") + +data class RecentSearchTerm( + @PrimaryKey(autoGenerate = true) + val searchTermId: Int = 0, + val keyword: String +) diff --git a/app/src/main/java/com/siba/searchmvvmpractice/remote/RetrofitService.kt b/app/src/main/java/com/siba/searchmvvmpractice/remote/RetrofitService.kt new file mode 100644 index 0000000..7e34253 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/remote/RetrofitService.kt @@ -0,0 +1,18 @@ +package com.siba.searchmvvmpractice.remote + +import com.siba.searchmvvmpractice.remote.model.UserCatalog +import com.siba.searchmvvmpractice.remote.model.UserRepositoryCatalog +import retrofit2.http.GET +import retrofit2.http.Query + +interface RetrofitService { + @GET("search/users") + suspend fun getUsers( + @Query("q") user: String + ): UserCatalog + + @GET("search/repositories") + suspend fun getRepositories( + @Query("q") repositoryName: String + ): UserRepositoryCatalog +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/remote/api/RetrofitBuilder.kt b/app/src/main/java/com/siba/searchmvvmpractice/remote/api/RetrofitBuilder.kt new file mode 100644 index 0000000..f038053 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/remote/api/RetrofitBuilder.kt @@ -0,0 +1,19 @@ +package com.siba.searchmvvmpractice.remote.api + +import com.siba.searchmvvmpractice.remote.RetrofitService +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitBuilder { + private const val URL = "https://api.github.com" + + private fun getRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + val retrofitService: RetrofitService = getRetrofit().create(RetrofitService::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/remote/model/UserCatalog.kt b/app/src/main/java/com/siba/searchmvvmpractice/remote/model/UserCatalog.kt new file mode 100644 index 0000000..d984d5a --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/remote/model/UserCatalog.kt @@ -0,0 +1,53 @@ +package com.siba.searchmvvmpractice.remote.model + +import com.google.gson.annotations.SerializedName + +data class UserCatalog( + @SerializedName("total_count") + val total_count: Int, + @SerializedName("incomplete_results") + val incomplete_results: Boolean, + @SerializedName("items") + val users: List +) + +data class Users( + @SerializedName("login") + val login: String, + @SerializedName("id") + val id: Int, + @SerializedName("node_id") + val node_id: String, + @SerializedName("avatar_url") + val avatar_url: String, + @SerializedName("gravatar_id") + val gravatar_id: String, + @SerializedName("url") + val url: String, + @SerializedName("html_url") + val html_url: String, + @SerializedName("followers_url") + val followers_url: String, + @SerializedName("following_url") + val following_url: String, + @SerializedName("gists_url") + val gists_url: String, + @SerializedName("starred_url") + val starred_url: String, + @SerializedName("subscriptions_url") + val subscriptions_url: String, + @SerializedName("organizations_url") + val organizations_url: String, + @SerializedName("repos_url") + val repos_url: String, + @SerializedName("events_url") + val events_url: String, + @SerializedName("received_events_url") + val received_events_url: String, + @SerializedName("type") + val type: String, + @SerializedName("site_admin") + val site_admin: Boolean, + @SerializedName("score") + val score: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/remote/model/UserRepositoryCatalog.kt b/app/src/main/java/com/siba/searchmvvmpractice/remote/model/UserRepositoryCatalog.kt new file mode 100644 index 0000000..8114b8e --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/remote/model/UserRepositoryCatalog.kt @@ -0,0 +1,19 @@ +package com.siba.searchmvvmpractice.remote.model + +import com.google.gson.annotations.SerializedName + +data class UserRepositoryCatalog( + @SerializedName("total_count") + val total_count: Int, + @SerializedName("incomplete_results") + val incomplete_results: Boolean, + @SerializedName("items") + val userRepository: List +) + +data class UserRepository( + @SerializedName("full_name") + val full_name: String, + @SerializedName("html_url") + val html_url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/repository/SearchRepository.kt b/app/src/main/java/com/siba/searchmvvmpractice/repository/SearchRepository.kt new file mode 100644 index 0000000..0185cc6 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/repository/SearchRepository.kt @@ -0,0 +1,22 @@ +package com.siba.searchmvvmpractice.repository + +import com.siba.searchmvvmpractice.local.dao.SearchTermDao +import com.siba.searchmvvmpractice.local.entity.RecentSearchTerm +import com.siba.searchmvvmpractice.remote.RetrofitService +import com.siba.searchmvvmpractice.remote.model.UserCatalog +import com.siba.searchmvvmpractice.remote.model.UserRepositoryCatalog + +class SearchRepository( + private val retrofitService: RetrofitService, + private val searchTermDao: SearchTermDao +) { + suspend fun fetchUser(userName: String): UserCatalog = retrofitService.getUsers(userName) + + suspend fun fetchRepo(repositoryName: String): UserRepositoryCatalog = retrofitService.getRepositories(repositoryName) + + suspend fun insert(recentSearchTerm: RecentSearchTerm) { + searchTermDao.insertKeyword(recentSearchTerm) + } + + fun getAll() = searchTermDao.getAllKeyword() +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/RepoAdapter.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/RepoAdapter.kt new file mode 100644 index 0000000..07c765d --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/RepoAdapter.kt @@ -0,0 +1,34 @@ +package com.siba.searchmvvmpractice.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import com.siba.searchmvvmpractice.BR +import com.siba.searchmvvmpractice.R +import com.siba.searchmvvmpractice.databinding.RepoItemBinding +import com.siba.searchmvvmpractice.remote.model.UserRepository + +class RepoAdapter : RecyclerView.Adapter.RepoViewHolder>() { + var data = mutableListOf() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder = + RepoViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false) + ) + + override fun onBindViewHolder(holder: RepoViewHolder, position: Int) { + holder.bind(data[position]) + } + + override fun getItemCount(): Int = data.size + + inner class RepoViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + private val binding: B = DataBindingUtil.bind(itemView)!! + fun bind(userRepository: UserRepository) { + binding.setVariable(BR.userRepository, userRepository) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/SearchTermAdapter.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/SearchTermAdapter.kt new file mode 100644 index 0000000..78bcc1b --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/SearchTermAdapter.kt @@ -0,0 +1,40 @@ +package com.siba.searchmvvmpractice.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import com.siba.searchmvvmpractice.BR +import com.siba.searchmvvmpractice.R +import com.siba.searchmvvmpractice.databinding.SearchTermItemBinding +import com.siba.searchmvvmpractice.local.entity.RecentSearchTerm + +class SearchTermAdapter : + RecyclerView.Adapter.SearchTermViewHolder>() { + var data = emptyList() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchTermViewHolder = + SearchTermViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.search_term_item, parent, false) + ) + + override fun onBindViewHolder(holder: SearchTermViewHolder, position: Int) { + holder.bind(data[position]) + } + + override fun getItemCount(): Int = data.size + + internal fun setData(recentSearchTerm: List) { + this.data = recentSearchTerm + notifyDataSetChanged() + } + + inner class SearchTermViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + private val binding: B = DataBindingUtil.bind(itemView)!! + fun bind(recentSearchTerm: RecentSearchTerm) { + binding.setVariable(BR.searchTerm, recentSearchTerm) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/UserAdapter.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/UserAdapter.kt new file mode 100644 index 0000000..71296a2 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/UserAdapter.kt @@ -0,0 +1,34 @@ +package com.siba.searchmvvmpractice.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import com.siba.searchmvvmpractice.BR +import com.siba.searchmvvmpractice.R +import com.siba.searchmvvmpractice.databinding.UserItemBinding +import com.siba.searchmvvmpractice.remote.model.Users + +class UserAdapter : RecyclerView.Adapter.UserViewHolder>() { + var data = mutableListOf() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder = + UserViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.user_item, parent, false) + ) + + override fun onBindViewHolder(holder: UserViewHolder, position: Int) { + holder.bind(data[position]) + } + + override fun getItemCount(): Int = data.size + + inner class UserViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + private val binding: B = DataBindingUtil.bind(itemView)!! + fun bind(users: Users) { + binding.setVariable(BR.users, users) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/ViewPagerAdapter.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/ViewPagerAdapter.kt new file mode 100644 index 0000000..f629580 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/adapter/ViewPagerAdapter.kt @@ -0,0 +1,20 @@ +package com.siba.searchmvvmpractice.ui.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import com.siba.searchmvvmpractice.ui.presentation.fragment.SearchRepoFragment +import com.siba.searchmvvmpractice.ui.presentation.fragment.SearchUserFragment + +class ViewPagerAdapter(fm: FragmentManager) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getCount(): Int = 2 + + override fun getItem(position: Int): Fragment { + return when (position) { + 0 -> SearchUserFragment() + else -> SearchRepoFragment() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/base/SearchViewModelFactory.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/base/SearchViewModelFactory.kt new file mode 100644 index 0000000..86b698f --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/base/SearchViewModelFactory.kt @@ -0,0 +1,22 @@ +package com.siba.searchmvvmpractice.ui.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.siba.searchmvvmpractice.repository.SearchRepository +import com.siba.searchmvvmpractice.ui.viewmodel.SearchViewModel + +@Suppress("UNCHECKED_CAST") +class SearchViewModelFactory(private val repository: SearchRepository) : + ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + /* if (modelClass.isAssignableFrom(SearchViewModel::class.java)) { + return SearchViewModel(repository) as T + } + throw IllegalArgumentException("Unknown class name")*/ + + // SearchViewModel(repository) as T에서 T는 ViewModel 로 타입이 일치한다. 하지만 에러가 나타나 suppress 해주었다. + require(modelClass.isAssignableFrom(SearchViewModel::class.java)){"Unknown class name"} + return SearchViewModel(repository) as T + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/activity/SearchActivity.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/activity/SearchActivity.kt new file mode 100644 index 0000000..4f77377 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/activity/SearchActivity.kt @@ -0,0 +1,103 @@ +package com.siba.searchmvvmpractice.ui.presentation.activity + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.siba.searchmvvmpractice.R +import com.siba.searchmvvmpractice.databinding.ActivitySearchBinding +import com.siba.searchmvvmpractice.databinding.SearchTermItemBinding +import com.siba.searchmvvmpractice.injection.Injection +import com.siba.searchmvvmpractice.ui.adapter.SearchTermAdapter +import com.siba.searchmvvmpractice.ui.adapter.ViewPagerAdapter +import com.siba.searchmvvmpractice.ui.viewmodel.SearchViewModel + +class SearchActivity : AppCompatActivity() { + + private lateinit var binding: ActivitySearchBinding + private lateinit var viewModel: SearchViewModel + + private lateinit var searchTermAdapter: SearchTermAdapter + + // TODO : 1. 최근검색어가 2개씩 저장되는 issue 처리 + // TODO : 2. 서버에서 데이터가져오는걸 실패할 경우 앱이 죽지 말고 있어야함 , 에러처리 주체도 생각해봐야 할 듯 + // TODO : 3. OFFLINE 캐싱 + // TODO : 4. base Factory rename to factory + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_search) + initViewModel() + initViews() + setViewPagerAdapter(supportFragmentManager) + setSearchView(binding.searchviewMain) + setSearchTermRecyclerView() + binding.lifecycleOwner = this + + } + + private fun initViewModel() { + viewModel = ViewModelProvider(this, Injection.provideSearchViewModelFactory(this)).get( + SearchViewModel::class.java + ) + } + + private fun initViews() { + searchTermAdapter = SearchTermAdapter() + viewModel.allSearch.observe(this) { + searchTermAdapter.setData(it) + } + searchTermAdapter.notifyDataSetChanged() + } + + private fun setSearchTermRecyclerView() { + binding.searchTermRecyclerviewMain.apply { + adapter = searchTermAdapter + layoutManager = LinearLayoutManager(this@SearchActivity) + addItemDecoration( + DividerItemDecoration( + this@SearchActivity, + LinearLayoutManager.VERTICAL + ) + ) + } + } + + private fun setSearchView(searchView: SearchView) { + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + viewModel.userName.value = query!! + search() + viewModel.saveSearchTerm() + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + }) + } + + private fun setViewPagerAdapter(fm: FragmentManager) { + binding.viewpagerMain.apply { + adapter = ViewPagerAdapter(fm) + } + binding.tabMain.apply { + setupWithViewPager(binding.viewpagerMain) + getTabAt(0)?.text = "User" + getTabAt(1)?.text = "Repository" + } + } + + private fun search() { + if (binding.tabMain.selectedTabPosition == 0) + viewModel.searchUser() + else + viewModel.searchRepo() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/fragment/SearchRepoFragment.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/fragment/SearchRepoFragment.kt new file mode 100644 index 0000000..3b5cd21 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/fragment/SearchRepoFragment.kt @@ -0,0 +1,56 @@ +package com.siba.searchmvvmpractice.ui.presentation.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.siba.searchmvvmpractice.R +import com.siba.searchmvvmpractice.databinding.FragmentSearchRepoBinding +import com.siba.searchmvvmpractice.databinding.RepoItemBinding +import com.siba.searchmvvmpractice.remote.model.UserRepository +import com.siba.searchmvvmpractice.ui.adapter.RepoAdapter +import com.siba.searchmvvmpractice.ui.viewmodel.SearchViewModel + +class SearchRepoFragment : Fragment() { + private lateinit var binding: FragmentSearchRepoBinding + + private val viewModel: SearchViewModel by activityViewModels() + private lateinit var repoAdapter: RepoAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_repo, container, false) + binding.viewModel = viewModel + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.lifecycleOwner = viewLifecycleOwner + repoAdapter = RepoAdapter() + setAdapter() + setObserver() + } + + private fun setObserver() { + viewModel.githubRepo.observe(viewLifecycleOwner) { + repoAdapter.data = it.userRepository as MutableList + repoAdapter.notifyDataSetChanged() + } + } + + private fun setAdapter() { + binding.searchRepoRecyclerview.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = repoAdapter + addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) + } + } + +} diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/fragment/SearchUserFragment.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/fragment/SearchUserFragment.kt new file mode 100644 index 0000000..05b781c --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/presentation/fragment/SearchUserFragment.kt @@ -0,0 +1,56 @@ +package com.siba.searchmvvmpractice.ui.presentation.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.siba.searchmvvmpractice.R +import com.siba.searchmvvmpractice.databinding.FragmentSearchUserBinding +import com.siba.searchmvvmpractice.databinding.UserItemBinding +import com.siba.searchmvvmpractice.remote.model.Users +import com.siba.searchmvvmpractice.ui.adapter.UserAdapter +import com.siba.searchmvvmpractice.ui.viewmodel.SearchViewModel + +class SearchUserFragment : Fragment() { + private lateinit var binding: FragmentSearchUserBinding + + private val viewModel: SearchViewModel by activityViewModels() + private lateinit var userAdapter: UserAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_user, container, false) + binding.viewModel = viewModel + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.lifecycleOwner = viewLifecycleOwner + userAdapter = UserAdapter() + setObserver() + setAdapter() + } + + private fun setAdapter() { + binding.searchUserRecyclerview.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = userAdapter + addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) + } + } + + fun setObserver() { + viewModel.githubUser.observe(viewLifecycleOwner) { + userAdapter.data = it.users as MutableList + userAdapter.notifyDataSetChanged() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/siba/searchmvvmpractice/ui/viewmodel/SearchViewModel.kt b/app/src/main/java/com/siba/searchmvvmpractice/ui/viewmodel/SearchViewModel.kt new file mode 100644 index 0000000..e6abab4 --- /dev/null +++ b/app/src/main/java/com/siba/searchmvvmpractice/ui/viewmodel/SearchViewModel.kt @@ -0,0 +1,57 @@ +package com.siba.searchmvvmpractice.ui.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.siba.searchmvvmpractice.local.entity.RecentSearchTerm +import com.siba.searchmvvmpractice.remote.model.UserCatalog +import com.siba.searchmvvmpractice.remote.model.UserRepositoryCatalog +import com.siba.searchmvvmpractice.repository.SearchRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class SearchViewModel( + private val repository: SearchRepository +) : ViewModel() { + + private val _userName = MutableLiveData() + val userName: MutableLiveData + get() = _userName + + private val _githubUser = MutableLiveData() + val githubUser: MutableLiveData + get() = _githubUser + + private val _githubRepo = MutableLiveData() + val githubRepo: MutableLiveData + get() = _githubRepo + + var allSearch: LiveData> = repository.getAll() + + fun searchUser() = viewModelScope.launch { + try { + _githubUser.value = repository.fetchUser(userName.value.toString()) + } catch (e: NullPointerException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + + fun searchRepo() = viewModelScope.launch { + try { + _githubRepo.value = repository.fetchRepo(userName.value.toString()) + } catch (e: NullPointerException) { + e.printStackTrace() + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + + fun saveSearchTerm() = viewModelScope.launch(Dispatchers.IO) { + val recentSearchTerm = RecentSearchTerm(keyword = userName.value.toString()) + repository.insert(recentSearchTerm) + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..44be649 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_recent_keyword.xml b/app/src/main/res/drawable/ic_recent_keyword.xml new file mode 100644 index 0000000..2b4ff02 --- /dev/null +++ b/app/src/main/res/drawable/ic_recent_keyword.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 4fc2444..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 0000000..07b011a --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search_repo.xml b/app/src/main/res/layout/fragment_search_repo.xml new file mode 100644 index 0000000..146adf1 --- /dev/null +++ b/app/src/main/res/layout/fragment_search_repo.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search_user.xml b/app/src/main/res/layout/fragment_search_user.xml new file mode 100644 index 0000000..7eb972e --- /dev/null +++ b/app/src/main/res/layout/fragment_search_user.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/repo_item.xml b/app/src/main/res/layout/repo_item.xml new file mode 100644 index 0000000..2328454 --- /dev/null +++ b/app/src/main/res/layout/repo_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_term_item.xml b/app/src/main/res/layout/search_term_item.xml new file mode 100644 index 0000000..c60a510 --- /dev/null +++ b/app/src/main/res/layout/search_term_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/user_item.xml b/app/src/main/res/layout/user_item.xml new file mode 100644 index 0000000..791727b --- /dev/null +++ b/app/src/main/res/layout/user_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 101c85f..235d3d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ SearchMVVMPractice + + Hello blank fragment \ No newline at end of file diff --git a/build.gradle b/build.gradle index 24d1382..73280a7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,22 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.4.10" + ext{ + version_kotlin = "1.3.72" + version_core = "1.3.1" + version_constraint_layout = "2.0.0-rc1" + version_lifecycle_extensions = "2.2.0" + version_material = "1.2.0" + version_navigation = "2.3.0" + version_lifecycle = "2.2.0" + version_room = "2.2.5" + } repositories { google() jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.1.0" + classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong