├── .gitignore ├── README.md ├── _config.yml ├── app ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hari │ │ └── kotlinflowsandcoroutines │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hari │ │ │ └── kotlinflowsandcoroutines │ │ │ ├── api │ │ │ └── GithubService.kt │ │ │ ├── ui │ │ │ ├── DashboardFragment.kt │ │ │ ├── MainActivity.kt │ │ │ └── search │ │ │ │ ├── RepoAdapter.kt │ │ │ │ ├── SearchFragment.kt │ │ │ │ ├── SearchRepository.kt │ │ │ │ └── SearchViewModel.kt │ │ │ ├── utils │ │ │ └── network │ │ │ │ ├── LiveDataCallAdapter.kt │ │ │ │ ├── LiveDataCallAdapterFactory.java │ │ │ │ ├── NetworkBoundResource.kt │ │ │ │ ├── NetworkState.kt │ │ │ │ ├── Resource.kt │ │ │ │ └── RetrofitExtensions.kt │ │ │ └── vo │ │ │ └── RepoSearchRespose.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── greenprogress.xml │ │ ├── ic_baseline_search_24.xml │ │ ├── ic_branch.xml │ │ ├── ic_language.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_search_black_24dp.xml │ │ ├── ic_stars.xml │ │ └── ic_user.xml │ │ ├── layout-v23 │ │ └── repo_view_item.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── content_main.xml │ │ ├── fragment_dashboard.xml │ │ ├── fragment_search.xml │ │ └── repo_view_item.xml │ │ ├── menu │ │ └── search_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── hari │ └── kotlinflowsandcoroutines │ └── ExampleUnitTest.kt ├── art └── device-2019-11-28-173356.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── versions.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Kotlin template 3 | # Compiled class file 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.war 15 | *.nar 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | ### Android template 24 | # Built application files 25 | # Files for the ART/Dalvik VM 26 | # Generated files 27 | bin/ 28 | gen/ 29 | # Gradle files 30 | .gradle/ 31 | build/ 32 | 33 | # Local configuration file (sdk path, etc) 34 | local.properties 35 | 36 | # Proguard folder generated by Eclipse 37 | proguard/ 38 | 39 | # Android Studio Navigation editor temp files 40 | .navigation/ 41 | 42 | # Android Studio captures folder 43 | captures/ 44 | 45 | # IntelliJ 46 | *.iml 47 | /.idea/ 48 | # Keystore files 49 | # Uncomment the following line if you do not want to check your keystore files in. 50 | #*.jks 51 | 52 | # External native build folder generated in Android Studio 2.2 and later 53 | .externalNativeBuild 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kotlin Flows and Coroutines 2 | ============================================== 3 | 4 | ```kotlin 5 | 6 | class SearchViewModel(searchRepository: SearchRepository) : ViewModel() { 7 | val query = MutableLiveData() 8 | 9 | @FlowPreview 10 | @ExperimentalCoroutinesApi 11 | val repo = query.asFlow() 12 | .debounce(300) 13 | .filter { 14 | it.trim().isEmpty().not() 15 | } 16 | .distinctUntilChanged() 17 | .flatMapLatest { 18 | searchRepository.searchRepo(it).asFlow() 19 | }.asLiveData() 20 | 21 | } 22 | 23 | ``` 24 | 25 | ### Screenshots 26 |

27 | Screenshot 28 |

29 | 30 | ### Tables of methods 31 | - Debounce: Here, the debounce operator is used with a time constant. The debounce operator handles the case when the user types “a”, “ab”, “abc”, in a very short time. So there will be too much network calls. But the user is finally interested in the result of the search “abc”. So, you must discard the results of “a” and “ab”. Ideally, there should be no network calls for “a” and “ab” as the user typed those in very short time. So, the debounce operator comes to the rescue. The debounce will wait for the provided time for doing anything, if any other search query comes in between that time, it will ignore the previous item and start waiting for that time again with the new search query. If nothing new comes in that given constant time, it will proceed with that search query for further processing. So, debounce only emit an item from an Observable if a particular timespan has passed without it emitting an another item. 32 | 33 | - Filter: The filter operator is used to filter the unwanted string like empty string in this case to avoid the unnecessary network call. 34 | 35 | - DistinctUntilChanged: The distinctUntilChanged operator is used to avoid the duplicate network calls. Let say the last on-going search query was “abc” and the user deleted “c” and again typed “c”. So again it’s “abc”. So if the network call is already going on with the search query “abc”, it will not make the duplicate call again with the search query “abc”. So, distinctUntilChanged suppress duplicate consecutive items emitted by the source Observable. 36 | 37 | - flatMapLatest: Here, the switchMap operator is used to avoid the network call results which are not needed more for displaying to the user. Let say the last search query was “ab” and there is an ongoing network call for “ab” and the user typed “abc”. Then you are no more interested in the result of “ab”. You are only interested in the result of “abc”. So, the switchMap comes to the rescue. It only provides the result for the last search query(most recent) and ignores the rest. 38 | 39 | License 40 | ------- 41 | 42 | Copyright 2019 Hari Singh Kulhari 43 | 44 | Licensed under the Apache License, Version 2.0 (the "License"); 45 | you may not use this file except in compliance with the License. 46 | You may obtain a copy of the License at 47 | 48 | http://www.apache.org/licenses/LICENSE-2.0 49 | 50 | Unless required by applicable law or agreed to in writing, software 51 | distributed under the License is distributed on an "AS IS" BASIS, 52 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 53 | See the License for the specific language governing permissions and 54 | limitations under the License. 55 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'androidx.navigation.safeargs' 5 | apply plugin: 'kotlin-kapt' 6 | android { 7 | compileSdkVersion build_versions.target_sdk 8 | buildToolsVersion build_versions.build_tools 9 | defaultConfig { 10 | applicationId "com.hari.kotlinflowsandcoroutines" 11 | minSdkVersion build_versions.min_sdk 12 | targetSdkVersion build_versions.target_sdk 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | dataBinding { 27 | enabled = true 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | 38 | } 39 | 40 | dependencies { 41 | implementation fileTree(dir: 'libs', include: ['*.jar']) 42 | 43 | implementation deps.support.app_compat 44 | implementation deps.support.design 45 | implementation deps.support.core_ktx 46 | implementation deps.kotlin.stdlib 47 | implementation deps.constraint_layout 48 | 49 | //Lifecycle 50 | kapt deps.lifecycle.compiler 51 | implementation deps.lifecycle.runtime 52 | implementation deps.lifecycle.viewmodel_ktx 53 | implementation deps.lifecycle.livedata_ktx 54 | 55 | //navigation 56 | implementation deps.navigation.fragment_ktx 57 | implementation deps.navigation.ui_ktx 58 | 59 | //Retrofit 60 | implementation deps.retrofit.runtime 61 | implementation deps.retrofit.gson 62 | implementation deps.okhttp.okhttp 63 | implementation deps.okhttp.okhttp_logging_interceptor 64 | 65 | //searchView 66 | implementation deps.materialsearchview 67 | 68 | //Glide 69 | implementation deps.glide.runtime 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hari/kotlinflowsandcoroutines/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hari.kotlinflowsandcoroutines 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.hari.kotlinflowsandcoroutines", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/api/GithubService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.api 18 | 19 | import androidx.lifecycle.LiveData 20 | import com.hari.kotlinflowsandcoroutines.utils.network.LiveDataCallAdapterFactory 21 | import com.hari.kotlinflowsandcoroutines.utils.network.Resource 22 | import com.hari.kotlinflowsandcoroutines.vo.RepoSearchResponse 23 | import okhttp3.OkHttpClient 24 | import okhttp3.logging.HttpLoggingInterceptor 25 | import retrofit2.Retrofit 26 | import retrofit2.converter.gson.GsonConverterFactory 27 | import retrofit2.http.GET 28 | import retrofit2.http.Query 29 | 30 | interface GithubService { 31 | @GET("search/repositories") 32 | fun searchRepos(@Query("q") query: String): LiveData> 33 | 34 | companion object { 35 | 36 | fun getGithubService(): GithubService { 37 | 38 | val interceptor = HttpLoggingInterceptor() 39 | interceptor.level = HttpLoggingInterceptor.Level.BODY 40 | return Retrofit.Builder() 41 | .baseUrl("https://api.github.com/") 42 | .addConverterFactory(GsonConverterFactory.create()) 43 | .addCallAdapterFactory(LiveDataCallAdapterFactory()) 44 | .client(OkHttpClient.Builder().addInterceptor(interceptor).build()) 45 | .build() 46 | .create(GithubService::class.java) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/ui/DashboardFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.ui 18 | 19 | import android.os.Bundle 20 | import android.view.LayoutInflater 21 | import android.view.View 22 | import android.view.ViewGroup 23 | import androidx.databinding.DataBindingUtil 24 | import androidx.fragment.app.Fragment 25 | import androidx.navigation.fragment.findNavController 26 | import com.hari.kotlinflowsandcoroutines.R 27 | import com.hari.kotlinflowsandcoroutines.databinding.FragmentDashboardBinding 28 | 29 | /** 30 | * A simple [Fragment] subclass as the default destination in the navigation. 31 | */ 32 | class DashboardFragment : Fragment() { 33 | private lateinit var mBinding: FragmentDashboardBinding 34 | override fun onCreateView( 35 | inflater: LayoutInflater, container: ViewGroup?, 36 | savedInstanceState: Bundle? 37 | ): View? { 38 | mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_dashboard, null, false) 39 | return mBinding.root 40 | } 41 | 42 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 43 | super.onViewCreated(view, savedInstanceState) 44 | mBinding.buttonSearch.setOnClickListener { 45 | findNavController().navigate(R.id.action_dashboardFragment_to_searchFragment) 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.ui 18 | 19 | import android.os.Bundle 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.databinding.DataBindingUtil 22 | import androidx.navigation.Navigation 23 | import androidx.navigation.findNavController 24 | import androidx.navigation.ui.NavigationUI 25 | import com.hari.kotlinflowsandcoroutines.R 26 | import com.hari.kotlinflowsandcoroutines.databinding.ActivityMainBinding 27 | 28 | 29 | class MainActivity : AppCompatActivity() { 30 | private lateinit var mBinding: ActivityMainBinding 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) 34 | setSupportActionBar(mBinding.toolbar) 35 | 36 | val navController = Navigation.findNavController(this, R.id.nav_host_fragment) 37 | NavigationUI.setupActionBarWithNavController(this, navController) 38 | } 39 | 40 | override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp() 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/ui/search/RepoAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.ui.search 18 | 19 | import android.view.LayoutInflater 20 | import android.view.ViewGroup 21 | import androidx.databinding.DataBindingUtil 22 | import androidx.recyclerview.widget.DiffUtil 23 | import androidx.recyclerview.widget.ListAdapter 24 | import androidx.recyclerview.widget.RecyclerView 25 | import com.bumptech.glide.Glide 26 | import com.hari.kotlinflowsandcoroutines.R 27 | import com.hari.kotlinflowsandcoroutines.databinding.RepoViewItemBinding 28 | import com.hari.kotlinflowsandcoroutines.vo.Repository 29 | 30 | class RepoAdapter : ListAdapter(DIFF()) { 31 | 32 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder = 33 | RepoViewHolder( 34 | DataBindingUtil.inflate( 35 | LayoutInflater.from(parent.context), 36 | R.layout.repo_view_item, 37 | parent, 38 | false 39 | ) 40 | ) 41 | 42 | override fun onBindViewHolder(holder: RepoViewHolder, position: Int) { 43 | holder.bind(getItem(position)) 44 | } 45 | 46 | class RepoViewHolder(private val viewItem: RepoViewItemBinding) : 47 | RecyclerView.ViewHolder(viewItem.root) { 48 | fun bind(repository: Repository) { 49 | viewItem.repo = repository 50 | Glide.with(viewItem.imageView.context).load(repository.owner.avatarUrl) 51 | .into(viewItem.imageView) 52 | } 53 | } 54 | 55 | companion object { 56 | class DIFF : DiffUtil.ItemCallback() { 57 | override fun areItemsTheSame(oldItem: Repository, newItem: Repository): Boolean { 58 | return oldItem.owner == newItem.owner 59 | && oldItem.name == newItem.name 60 | 61 | } 62 | 63 | override fun areContentsTheSame(oldItem: Repository, newItem: Repository): Boolean { 64 | return oldItem.description == newItem.description 65 | && oldItem.stargazersCount == newItem.stargazersCount 66 | } 67 | 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/ui/search/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.ui.search 18 | 19 | import android.os.Bundle 20 | import android.os.Handler 21 | import android.view.* 22 | import androidx.databinding.DataBindingUtil 23 | import androidx.fragment.app.Fragment 24 | import androidx.lifecycle.Observer 25 | import androidx.lifecycle.ViewModel 26 | import androidx.lifecycle.ViewModelProvider 27 | import com.hari.kotlinflowsandcoroutines.R 28 | import com.hari.kotlinflowsandcoroutines.api.GithubService 29 | import com.hari.kotlinflowsandcoroutines.databinding.FragmentSearchBinding 30 | import com.hari.kotlinflowsandcoroutines.ui.MainActivity 31 | import com.miguelcatalan.materialsearchview.MaterialSearchView 32 | import kotlinx.coroutines.ExperimentalCoroutinesApi 33 | import kotlinx.coroutines.FlowPreview 34 | 35 | 36 | class SearchFragment : Fragment() { 37 | private lateinit var searchView: MaterialSearchView 38 | private lateinit var mBinding: FragmentSearchBinding 39 | 40 | private val viewModel: SearchViewModel by lazy { 41 | ViewModelProvider(this, factory).get(SearchViewModel::class.java) 42 | } 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | super.onCreate(savedInstanceState) 46 | setHasOptionsMenu(true) 47 | } 48 | 49 | override fun onCreateView( 50 | inflater: LayoutInflater, container: ViewGroup?, 51 | savedInstanceState: Bundle? 52 | ): View? { 53 | mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_search, null, false) 54 | setUpSearchView() 55 | mBinding.lifecycleOwner = viewLifecycleOwner 56 | mBinding.viewModel = viewModel 57 | return mBinding.root 58 | } 59 | 60 | private fun setUpSearchView() { 61 | searchView = (activity as MainActivity).findViewById(R.id.search_view) 62 | searchView.showSearch(false) 63 | Handler().postDelayed({ 64 | searchView.setQuery("Android", true) 65 | }, 500) 66 | 67 | } 68 | 69 | 70 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 71 | super.onViewCreated(view, savedInstanceState) 72 | setUpAdapter() 73 | observeSearchQuery() 74 | 75 | } 76 | 77 | @ExperimentalCoroutinesApi 78 | @FlowPreview 79 | private fun setUpAdapter() { 80 | val adapter = RepoAdapter() 81 | mBinding.recyclerViewRepo.adapter = adapter 82 | 83 | viewModel.repo.observe(viewLifecycleOwner, Observer { 84 | if (it.status.isSuccessful()) 85 | adapter.submitList(it.data?.repositories) 86 | }) 87 | } 88 | 89 | private fun observeSearchQuery() { 90 | searchView.setOnQueryTextListener(object : MaterialSearchView.OnQueryTextListener { 91 | override fun onQueryTextSubmit(query: String?): Boolean { 92 | return true 93 | } 94 | 95 | override fun onQueryTextChange(newText: String?): Boolean { 96 | viewModel.query.value = newText 97 | return true 98 | } 99 | 100 | }) 101 | } 102 | 103 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 104 | inflater.inflate(R.menu.search_menu, menu) 105 | val item = menu.findItem(R.id.action_search) 106 | searchView.setMenuItem(item) 107 | 108 | } 109 | 110 | 111 | private val factory = object : ViewModelProvider.Factory { 112 | override fun create(modelClass: Class): T { 113 | return SearchViewModel( 114 | SearchRepository(GithubService.getGithubService()) 115 | ) as T 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/ui/search/SearchRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.ui.search 18 | 19 | import androidx.lifecycle.LiveData 20 | import com.hari.kotlinflowsandcoroutines.api.GithubService 21 | import com.hari.kotlinflowsandcoroutines.utils.network.NetworkBoundResource 22 | import com.hari.kotlinflowsandcoroutines.utils.network.Resource 23 | import com.hari.kotlinflowsandcoroutines.vo.RepoSearchResponse 24 | 25 | class SearchRepository(val githubService: GithubService) { 26 | 27 | fun searchRepo(q: String): LiveData> { 28 | return object : NetworkBoundResource() { 29 | override fun createCall(): LiveData> { 30 | return githubService.searchRepos(q) 31 | } 32 | }.asLiveData() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/ui/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.ui.search 18 | 19 | import androidx.lifecycle.MutableLiveData 20 | import androidx.lifecycle.ViewModel 21 | import androidx.lifecycle.asFlow 22 | import androidx.lifecycle.asLiveData 23 | import kotlinx.coroutines.ExperimentalCoroutinesApi 24 | import kotlinx.coroutines.FlowPreview 25 | import kotlinx.coroutines.flow.debounce 26 | import kotlinx.coroutines.flow.distinctUntilChanged 27 | import kotlinx.coroutines.flow.filter 28 | import kotlinx.coroutines.flow.flatMapLatest 29 | 30 | class SearchViewModel(searchRepository: SearchRepository) : ViewModel() { 31 | val query = MutableLiveData() 32 | 33 | @FlowPreview 34 | @ExperimentalCoroutinesApi 35 | val repo = query.asFlow() 36 | .debounce(300) 37 | .filter { 38 | it.trim().isEmpty().not() 39 | } 40 | .distinctUntilChanged() 41 | .flatMapLatest { 42 | searchRepository.searchRepo(it).asFlow() 43 | }.asLiveData() 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/utils/network/LiveDataCallAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.utils.network 18 | 19 | 20 | import androidx.lifecycle.LiveData 21 | import retrofit2.Call 22 | import retrofit2.CallAdapter 23 | import retrofit2.Callback 24 | import retrofit2.Response 25 | import java.lang.reflect.Type 26 | import java.util.concurrent.atomic.AtomicBoolean 27 | 28 | /** 29 | * A Retrofit adapter that converts the Call into a LiveData of ApiResponse. 30 | * @param 31 | */ 32 | class LiveDataCallAdapter(private val responseType: Type) : 33 | CallAdapter>> { 34 | 35 | override fun responseType(): Type { 36 | return responseType 37 | } 38 | 39 | override fun adapt(call: Call): LiveData> { 40 | return object : LiveData>() { 41 | var started = AtomicBoolean(false) 42 | 43 | override fun onActive() { 44 | super.onActive() 45 | if (started.compareAndSet(false, true)) { 46 | call.enqueue(object : Callback { 47 | override fun onResponse(call: Call, response: Response) { 48 | postValue(response.toResource()) 49 | } 50 | 51 | override fun onFailure(call: Call, throwable: Throwable) { 52 | postValue(Resource.error(throwable.message)) 53 | } 54 | }) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/utils/network/LiveDataCallAdapterFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.utils.network; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.lifecycle.LiveData; 21 | 22 | import java.lang.annotation.Annotation; 23 | import java.lang.reflect.ParameterizedType; 24 | import java.lang.reflect.Type; 25 | 26 | import retrofit2.CallAdapter; 27 | import retrofit2.Retrofit; 28 | 29 | public class LiveDataCallAdapterFactory extends CallAdapter.Factory { 30 | 31 | @Override 32 | public CallAdapter get(@NonNull Type returnType, @NonNull Annotation[] annotations, @NonNull Retrofit retrofit) { 33 | if (getRawType(returnType) != LiveData.class) { 34 | return null; 35 | } 36 | Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType); 37 | Class rawObservableType = getRawType(observableType); 38 | if (rawObservableType != Resource.class) { 39 | throw new IllegalArgumentException("type must be a resource"); 40 | } 41 | if (!(observableType instanceof ParameterizedType)) { 42 | throw new IllegalArgumentException("resource must be parameterized"); 43 | } 44 | Type bodyType = getParameterUpperBound(0, (ParameterizedType) observableType); 45 | return new LiveDataCallAdapter<>(bodyType); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/utils/network/NetworkBoundResource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.utils.network 18 | 19 | import androidx.annotation.MainThread 20 | import androidx.lifecycle.LiveData 21 | import androidx.lifecycle.MediatorLiveData 22 | 23 | /** 24 | * A generic class that can provide a resource backed the network. 25 | * 26 | * You can read more about it in the [Architecture 27 | * Guide](https://developer.android.com/arch). 28 | * 29 | * @param */ 30 | abstract class NetworkBoundResource { 31 | 32 | private val result = MediatorLiveData>() 33 | 34 | init { 35 | setValue(Resource.loading()) 36 | fetchFromNetwork() 37 | } 38 | 39 | @MainThread 40 | private fun setValue(newValue: Resource) { 41 | if (result.value != newValue) { 42 | result.value = newValue 43 | } 44 | } 45 | 46 | private fun fetchFromNetwork() { 47 | val apiResponse = createCall() 48 | result.addSource(apiResponse) { response -> 49 | result.removeSource(apiResponse) 50 | 51 | response.apply { 52 | if (status.isSuccessful()) { 53 | setValue( 54 | Resource( 55 | NetworkState.SUCCESS, 56 | this.data 57 | ) 58 | ) 59 | } else { 60 | onFetchFailed() 61 | setValue( 62 | Resource.error( 63 | errorMessage 64 | ) 65 | ) 66 | } 67 | } 68 | 69 | } 70 | } 71 | 72 | private fun onFetchFailed() {} 73 | 74 | fun asLiveData() = result as LiveData> 75 | 76 | @MainThread 77 | protected abstract fun createCall(): LiveData> 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/utils/network/NetworkState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.utils.network 18 | 19 | /** 20 | * NetworkState of a resource that is provided to the UI. 21 | * 22 | * 23 | * These are usually created by the Repository classes where they return 24 | * `LiveData>` to pass back the latest data to the UI with its fetch status. 25 | */ 26 | enum class NetworkState { 27 | SUCCESS, 28 | ERROR, 29 | LOADING; 30 | 31 | /** 32 | * Returns `true` if the [NetworkState] is success else `false`. 33 | */ 34 | fun isSuccessful() = this == SUCCESS 35 | 36 | /** 37 | * Returns `true` if the [NetworkState] is loading else `false`. 38 | */ 39 | fun isLoading() = this == LOADING 40 | 41 | /** 42 | * Returns `true` if the [NetworkState] is loading else `false`. 43 | */ 44 | fun isFailed() = this == ERROR 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/utils/network/Resource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.utils.network 18 | 19 | /** 20 | * A generic class that holds a value with its loading status. 21 | * @param 22 | */ 23 | data class Resource( 24 | var status: NetworkState, 25 | var data: ResultType? = null, 26 | var errorMessage: String? = null 27 | ) { 28 | 29 | companion object { 30 | /** 31 | * Creates [Resource] object with `SUCCESS` status and [data]. 32 | */ 33 | fun success(data: ResultType): Resource = 34 | Resource( 35 | NetworkState.SUCCESS, 36 | data 37 | ) 38 | 39 | /** 40 | * Creates [Resource] object with `LOADING` status to notify 41 | * the UI to showing loading. 42 | */ 43 | fun loading(): Resource = 44 | Resource(NetworkState.LOADING) 45 | 46 | /** 47 | * Creates [Resource] object with `ERROR` status and [message]. 48 | */ 49 | fun error(message: String?): Resource = 50 | Resource( 51 | NetworkState.ERROR, 52 | errorMessage = message 53 | ) 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/utils/network/RetrofitExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.utils.network 18 | 19 | import retrofit2.Response 20 | import retrofit2.Retrofit 21 | 22 | /** 23 | * Synthetic sugaring to create Retrofit Service. 24 | */ 25 | inline fun Retrofit.create(): T = create(T::class.java) 26 | 27 | /** 28 | * Converts Retrofit [Response] to [Resource] which provides state 29 | * and data to the UI. 30 | */ 31 | fun Response.toResource(): Resource { 32 | val error = errorBody()?.byteString().toString() 33 | return when { 34 | isSuccessful -> { 35 | val body = body() 36 | when { 37 | body != null -> Resource.success(body) 38 | else -> { 39 | Resource.error(error) 40 | } 41 | } 42 | } 43 | else -> Resource.error(error) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/kotlinflowsandcoroutines/vo/RepoSearchRespose.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hari Singh Kulhari 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hari.kotlinflowsandcoroutines.vo 18 | 19 | import com.google.gson.annotations.SerializedName 20 | 21 | data class RepoSearchResponse( 22 | @SerializedName("incomplete_results") 23 | val incompleteResults: Boolean, 24 | @SerializedName("items") 25 | val repositories: List, 26 | @SerializedName("total_count") 27 | val totalCount: Int 28 | ) 29 | 30 | data class Repository( 31 | @SerializedName("created_at") 32 | val createdAt: String, 33 | @SerializedName("default_branch") 34 | val defaultBranch: String, 35 | @SerializedName("description") 36 | val description: String, 37 | @SerializedName("fork") 38 | val fork: Boolean, 39 | @SerializedName("forks_count") 40 | val forksCount: Int, 41 | @SerializedName("full_name") 42 | val fullName: String, 43 | @SerializedName("homepage") 44 | val homepage: String, 45 | @SerializedName("html_url") 46 | val htmlUrl: String, 47 | @SerializedName("id") 48 | val id: Int, 49 | @SerializedName("language") 50 | val language: String, 51 | @SerializedName("master_branch") 52 | val masterBranch: String, 53 | @SerializedName("name") 54 | val name: String, 55 | @SerializedName("node_id") 56 | val nodeId: String, 57 | @SerializedName("open_issues_count") 58 | val openIssuesCount: Int, 59 | @SerializedName("owner") 60 | val owner: Owner, 61 | @SerializedName("private") 62 | val `private`: Boolean, 63 | @SerializedName("pushed_at") 64 | val pushedAt: String, 65 | @SerializedName("score") 66 | val score: Double, 67 | @SerializedName("size") 68 | val size: Int, 69 | @SerializedName("stargazers_count") 70 | val stargazersCount: Int, 71 | @SerializedName("updated_at") 72 | val updatedAt: String, 73 | @SerializedName("url") 74 | val url: String, 75 | @SerializedName("watchers_count") 76 | val watchersCount: Int 77 | ) 78 | 79 | data class Owner( 80 | @SerializedName("avatar_url") 81 | val avatarUrl: String, 82 | @SerializedName("gravatar_id") 83 | val gravatarId: String, 84 | @SerializedName("id") 85 | val id: Int, 86 | @SerializedName("login") 87 | val login: String, 88 | @SerializedName("node_id") 89 | val nodeId: String, 90 | @SerializedName("received_events_url") 91 | val receivedEventsUrl: String, 92 | @SerializedName("type") 93 | val type: String, 94 | @SerializedName("url") 95 | val url: String 96 | ) 97 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/greenprogress.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_search_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_branch.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_language.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stars.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout-v23/repo_view_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 22 | 23 | 28 | 29 | 36 | 37 | 49 | 50 | 64 | 65 | 81 | 82 | 99 | 100 | 117 | 118 | 135 | 136 | 137 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_dashboard.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 |