From 9983c8d53a93003b754eba94d3b584bf54a2cc59 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Mon, 4 Feb 2019 17:01:22 +0300 Subject: [PATCH 01/11] Coroutines added --- app/build.gradle | 6 +- .../pavel/gifrandom/data/Repository.kt | 6 +- .../pavel/gifrandom/data/RepositoryImpl.kt | 65 +++++++++---------- .../haretskiy/pavel/gifrandom/di/Modules.kt | 4 +- .../haretskiy/pavel/gifrandom/rest/RestApi.kt | 17 ++--- .../pavel/gifrandom/rest/RestApiImpl.kt | 4 +- .../gifrandom/utils/pagging/GifsDataSource.kt | 65 ++++++++++--------- 7 files changed, 76 insertions(+), 91 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c8d349b..d179dd0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,11 +68,7 @@ dependencies { // Retrofit2 implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}" implementation "com.squareup.retrofit2:converter-gson:${retrofitVersion}" - implementation "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" - - // RxJava2 - implementation "io.reactivex.rxjava2:rxandroid:2.1.0" - implementation "io.reactivex.rxjava2:rxjava:2.2.5" + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' // Glide implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.10" diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt index 62352ab..05e1c4b 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt @@ -1,9 +1,7 @@ package com.haretskiy.pavel.gifrandom.data interface Repository { -// fun loadTrendingGifs(rating: String, offset: String = ZERO_OFFSET): Observable> -// fun loadGifsByWord(word: String, rating: String, offset: String = ZERO_OFFSET): Observable> - fun loadTrendingGifs(rating: String, offset: String, resultCallback: RepositoryImpl.ResultCallback) - fun loadGifsByWord(word: String, rating: String, offset: String, resultCallback: RepositoryImpl.ResultCallback) + fun loadTrendingGifs(rating: String, offset: String) : List + fun loadGifsByWord(word: String, rating: String, offset: String) : List } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt index 827b19c..3f1a64d 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt @@ -1,44 +1,37 @@ package com.haretskiy.pavel.gifrandom.data +import android.util.Log import com.haretskiy.pavel.gifrandom.EMPTY_STRING import com.haretskiy.pavel.gifrandom.models.GifResponse import com.haretskiy.pavel.gifrandom.rest.RestApiImpl -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.* -class RepositoryImpl( - private val restApi: RestApiImpl) : Repository { - - override fun loadTrendingGifs(rating: String, offset: String, resultCallback: ResultCallback) { - load(restApi.loadGifs(rating, offset), resultCallback) - } - - override fun loadGifsByWord(word: String, rating: String, offset: String, resultCallback: ResultCallback) { - load(restApi.loadGifsByWord(word, rating, offset), resultCallback) - } - - private fun load(obs: Observable, resultCallback: ResultCallback) { - obs.subscribeOn(Schedulers.io()) - .map { - it.data.map { - it.images?.original?.url ?: EMPTY_STRING - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - if (it != null) { - resultCallback.onResult(it) - } - }, - { - resultCallback.onResult(emptyList()) - }) - } - - interface ResultCallback { - fun onResult(list: List) +class RepositoryImpl(private val restApi: RestApiImpl) : Repository { + + override fun loadTrendingGifs(rating: String, offset: String) = load(restApi.loadGifsAsync( + rating, + offset)) + + override fun loadGifsByWord(word: String, + rating: String, + offset: String) = load(restApi.loadGifsByWordAsync(word, + rating, + offset)) + + private fun load(deferred: Deferred): List = runBlocking { + GlobalScope.async(CoroutineExceptionHandler { _, exception -> + Log.d("RepositoryImpl", "Caught $exception") + }, CoroutineStart.DEFAULT, null, { + val responseData = deferred.await() + Log.d("RepositoryImpl", "On success") + convertData(responseData) + }) + .await() } - + + private suspend fun convertData(responseData: GifResponse) = GlobalScope.async(Dispatchers.Default, + CoroutineStart.DEFAULT, + null, + { responseData.data.map { it.images?.original?.url ?: EMPTY_STRING } }).await() + } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt index 4ffe446..c87ef59 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt @@ -13,13 +13,13 @@ import com.haretskiy.pavel.gifrandom.utils.pagging.DiffCallBack import com.haretskiy.pavel.gifrandom.utils.pagging.GifsSourceFactory import com.haretskiy.pavel.gifrandom.viewModels.DetailViewModel import com.haretskiy.pavel.gifrandom.viewModels.MainViewModel +import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidApplication import org.koin.android.viewmodel.ext.koin.viewModel import org.koin.dsl.module.Module import org.koin.dsl.module.module import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory val restModule: Module = module(definition = { @@ -37,7 +37,7 @@ val restModule: Module = module(definition = { .baseUrl(BASE_URL) .client(get()) .addConverterFactory(GsonConverterFactory.create(get())) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build() .create(RestApi::class.java) } diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt index 9baaf7d..bc61c0f 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt @@ -2,21 +2,16 @@ package com.haretskiy.pavel.gifrandom.rest import com.haretskiy.pavel.gifrandom.ZERO_OFFSET import com.haretskiy.pavel.gifrandom.models.GifResponse -import io.reactivex.Observable +import kotlinx.coroutines.Deferred import retrofit2.http.GET import retrofit2.http.Query - interface RestApi { - + @GET("gifs/trending") - fun loadGifs(@Query("limit") limit: Int, - @Query("rating") rating: String, - @Query("offset") offset: String = ZERO_OFFSET): Observable - + fun loadGifsAsync(@Query("limit") limit: Int, @Query("rating") rating: String, @Query("offset") offset: String = ZERO_OFFSET): Deferred + @GET("gifs/search") - fun loadGifsBySearchWord(@Query("q") searchWord: String, - @Query("limit") limit: Int, - @Query("rating") rating: String, - @Query("offset") offset: String = ZERO_OFFSET): Observable + fun loadGifsBySearchWordAsync(@Query("q") searchWord: String, @Query("limit") limit: Int, @Query("rating") rating: String, @Query( + "offset") offset: String = ZERO_OFFSET): Deferred } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt index e635f34..bc45af9 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt @@ -5,8 +5,8 @@ import com.haretskiy.pavel.gifrandom.ZERO_OFFSET class RestApiImpl(private val restApi: RestApi) { - fun loadGifs(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifs(LIMIT, rating, offset) + fun loadGifsAsync(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsAsync(LIMIT, rating, offset) - fun loadGifsByWord(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWord(word, LIMIT, rating, offset) + fun loadGifsByWordAsync(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWordAsync(word, LIMIT, rating, offset) } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt index 8bfabcf..337d629 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt @@ -3,50 +3,53 @@ package com.haretskiy.pavel.gifrandom.utils.pagging import android.arch.paging.PositionalDataSource import com.haretskiy.pavel.gifrandom.ZERO_OFFSET import com.haretskiy.pavel.gifrandom.data.Repository -import com.haretskiy.pavel.gifrandom.data.RepositoryImpl.ResultCallback - -class GifsDataSource( - private val repository: Repository, - private var gifsLoadedCallback: GifsLoadedCallback, - private var rating: String, - private var word: String) : PositionalDataSource() { +class GifsDataSource(private val repository: Repository, + private var gifsLoadedCallback: GifsLoadedCallback, + private var rating: String, + private var word: String) : PositionalDataSource() { + override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { gifsLoadedCallback.onStartPageLoad() when { - word.isEmpty() -> repository.loadTrendingGifs(rating, params.startPosition.toString(), resultCallback(callback)) - else -> repository.loadGifsByWord(word, rating, params.startPosition.toString(), resultCallback(callback)) + word.isEmpty() -> handleLoadRange(repository.loadTrendingGifs(rating, + params.startPosition.toString()), callback) + else -> handleLoadRange(repository.loadGifsByWord(word, + rating, + params.startPosition.toString()), callback) } } - + + private fun handleLoadRange(listOfGifs: List, + callback: LoadRangeCallback) { + callback.onResult(listOfGifs) + gifsLoadedCallback.onFinishPageLoad() + } + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { gifsLoadedCallback.onStartInitialLoad() when { - word.isEmpty() -> repository.loadTrendingGifs(rating, ZERO_OFFSET, initialResultCallback(params, callback)) - else -> repository.loadGifsByWord(word, rating, ZERO_OFFSET, initialResultCallback(params, callback)) + word.isEmpty() -> handleLoadInitial(repository.loadTrendingGifs(rating, ZERO_OFFSET), + params, + callback) + else -> handleLoadInitial(repository.loadGifsByWord(word, rating, ZERO_OFFSET), + params, + callback) } } - - private fun initialResultCallback(params: LoadInitialParams, callback: LoadInitialCallback) = object : ResultCallback { - override fun onResult(list: List) = try { - var startPos = params.requestedStartPosition - when { - startPos < 0 -> startPos = 0 - } - callback.onResult(list, startPos) - gifsLoadedCallback.onFinishInitialLoad() - } catch (ex: Exception) { - ex.printStackTrace() - } - } - - private fun resultCallback(callback: LoadRangeCallback) = object : ResultCallback { - override fun onResult(list: List) { - callback.onResult(list) - gifsLoadedCallback.onFinishPageLoad() + + private fun handleLoadInitial(listOfGifs: List, + params: LoadInitialParams, + callback: LoadInitialCallback) { + var startPos = params.requestedStartPosition + when { + startPos < 0 -> startPos = 0 } + callback.onResult(listOfGifs, startPos) + gifsLoadedCallback.onFinishInitialLoad() + } - + interface GifsLoadedCallback { fun onStartInitialLoad() {} fun onFinishInitialLoad() {} From 615ff033f6fcd767b373d8d03c8a3601d2d69171 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sat, 9 Feb 2019 21:37:48 +0300 Subject: [PATCH 02/11] com.android.tools.build:gradle:3.3.0 -> 3.3.1 --- build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index f2aa1a9..1dad410 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.20' + ext.kotlin_version = '1.3.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:3.3.0' } } From f497a71c59dd063cb42364d9440c8006ed2c7ee5 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sun, 10 Feb 2019 12:58:01 +0300 Subject: [PATCH 03/11] Connectivity added --- app/src/main/AndroidManifest.xml | 1 + .../haretskiy/pavel/gifrandom/Extensions.kt | 30 +++++++-- .../gifrandom/activities/MainActivity.kt | 25 ++++--- .../haretskiy/pavel/gifrandom/di/Modules.kt | 8 ++- .../pavel/gifrandom/rest/JsonInterceptor.kt | 6 +- .../pavel/gifrandom/utils/Connectivity.kt | 52 ++++++++++++++ .../gifrandom/utils/ConnectivityLiveData.kt | 67 +++++++++++++++++++ .../gifrandom/viewModels/MainViewModel.kt | 40 +++++------ app/src/main/res/values/strings.xml | 2 + 9 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt create mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5d08a75..36f25c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.haretskiy.pavel.gifrandom"> + LiveData.distinctUntilChanged(): LiveData { + val mutableLiveData: MediatorLiveData = MediatorLiveData() + var latestValue: T? = null + mutableLiveData.addSource(this) { + if (latestValue != it) { + mutableLiveData.value = it + latestValue = it + } + } + return mutableLiveData +} + +fun LiveData.map(function: MapperFunction): LiveData { + return Transformations.map(this, function) +} + +typealias MapperFunction = (T) -> O \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt index 96566b9..8a44ee3 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt @@ -5,6 +5,7 @@ import android.arch.paging.PagedList import android.databinding.DataBindingUtil import android.os.Bundle import android.support.v7.app.AppCompatActivity +import android.widget.Toast import com.haretskiy.pavel.gifrandom.R import com.haretskiy.pavel.gifrandom.ZERO import com.haretskiy.pavel.gifrandom.adapters.GifAdapter @@ -16,25 +17,25 @@ import org.koin.android.ext.android.inject import org.koin.android.viewmodel.ext.android.viewModel class MainActivity : AppCompatActivity() { - + private val mainViewModel: MainViewModel by viewModel() - + private val adapter: GifAdapter by inject() - + private val binding: ActivityMainBinding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_main) as ActivityMainBinding } - + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + binding.model = mainViewModel mainViewModel.setBinding(binding) - + initRecyclerView() initToolbar() } - + private fun initRecyclerView() { mainViewModel.pagedListLiveData.observe(this, Observer> { urls -> adapter.submitList(urls) @@ -44,11 +45,17 @@ class MainActivity : AppCompatActivity() { rv_gifs.scrollToPosition(ZERO) } }) + mainViewModel.observeOnline() + .observe(this, Observer { + Toast.makeText(this@MainActivity, if(it == true) getString(R.string.online_state) else getString( + R.string.offline_state), Toast.LENGTH_SHORT) + .show() + }) rv_gifs.adapter = adapter } - + private fun initToolbar() { setSupportActionBar(toolbar) } - + } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt index c87ef59..7d2f546 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt @@ -1,5 +1,7 @@ package com.haretskiy.pavel.gifrandom.di +import android.content.Context +import android.net.ConnectivityManager import com.google.gson.GsonBuilder import com.haretskiy.pavel.gifrandom.BASE_URL import com.haretskiy.pavel.gifrandom.adapters.GifAdapter @@ -42,6 +44,10 @@ val restModule: Module = module(definition = { .create(RestApi::class.java) } single { RestApiImpl(get()) } + + single { Connectivity(androidApplication(), get()) } + + factory { androidApplication().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } }) val appModule: Module = module(definition = { @@ -55,7 +61,7 @@ val appModule: Module = module(definition = { }) val viewModelModule: Module = module(definition = { - viewModel { MainViewModel(androidApplication(), get()) } + viewModel { MainViewModel(androidApplication(), get(), get()) } viewModel {parameterList -> DetailViewModel(get(), parameterList[0]) } diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt index 5df51a0..61d7ac7 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt @@ -34,14 +34,14 @@ class JsonInterceptor : Interceptor { val max = 4000 * (i + 1) if (max >= length) { Log.d(RESPONSE, "chunk $i of $chunkCount:") - Log.e(RESPONSE, prettyString.substring(4000 * i)) + Log.w(RESPONSE, prettyString.substring(4000 * i)) } else { Log.d(RESPONSE, "chunk $i of $chunkCount:") - Log.e(RESPONSE, prettyString.substring(4000 * i, max)) + Log.w(RESPONSE, prettyString.substring(4000 * i, max)) } } } else { - Log.e(RESPONSE, prettyString) + Log.w(RESPONSE, prettyString) } Log.d(END, "Received response for ${response.request().url()} for ${(t2 - t1) / 1e6} milliseconds ") diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt new file mode 100644 index 0000000..89957f8 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt @@ -0,0 +1,52 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.util.Log +import com.haretskiy.pavel.gifrandom.distinctUntilChanged +import com.haretskiy.pavel.gifrandom.map + +class Connectivity(context: Context, private val manager: ConnectivityManager) { + + private val connectivityLiveData = ConnectivityLiveData(context, + IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION), + null, + null) + + companion object { + const val TAG = "Connectivity" + } + + @Suppress("DEPRECATION") + @SuppressLint("MissingPermission") + fun isOnline(): Boolean { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + val activeNetworks = manager.allNetworks + for (network in activeNetworks) { + val networkInfo = manager.getNetworkInfo(network) + if (networkInfo != null && networkInfo.isConnected) { + Log.d(TAG, "isOnline true: $networkInfo") + return true + } + } + } else { + val networksInfo = this.manager.allNetworkInfo + if (networksInfo != null) for (networkInfo in networksInfo) { + if (networkInfo != null && networkInfo.state == NetworkInfo.State.CONNECTED) { + Log.d(TAG, "isOnline true: $networkInfo.toString()") + return true + } + } + } + + Log.d(TAG, "isOnline false") + + return false + } + + fun onlineChanges() = connectivityLiveData.map { isOnline() }.distinctUntilChanged() + +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt new file mode 100644 index 0000000..d614a55 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt @@ -0,0 +1,67 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.Observer +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.util.Log +import com.haretskiy.pavel.gifrandom.utils.Connectivity.Companion.TAG + +class ConnectivityLiveData(private val context: Context, + private val intentFilter: IntentFilter, + private val broadcastPermission: String?, + private val schedulerHandler: Handler?) : LiveData() { + + private lateinit var broadcastReceiver: BroadcastReceiver + + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner, observer) + + observe() + } + + override fun observeForever(observer: Observer) { + super.observeForever(observer) + + observe() + } + + private fun observe() { + try { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + postValue(intent) + } + } + + context.registerReceiver(broadcastReceiver, + intentFilter, + broadcastPermission, + schedulerHandler) + } catch (t: Throwable) { + Log.e(TAG, t.toString()) + } + } + + private fun removeObserver() { + try { + context.unregisterReceiver(broadcastReceiver) + } catch (t: Throwable) { + Log.e(TAG, t.toString()) + } + } + + override fun removeObserver(observer: Observer) { + super.removeObserver(observer) + removeObserver() + } + + override fun removeObservers(owner: LifecycleOwner) { + super.removeObservers(owner) + removeObserver() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt index b34e47b..49d7987 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt @@ -11,61 +11,63 @@ import android.view.View import android.widget.AdapterView import com.haretskiy.pavel.gifrandom.* import com.haretskiy.pavel.gifrandom.databinding.ActivityMainBinding +import com.haretskiy.pavel.gifrandom.utils.Connectivity import com.haretskiy.pavel.gifrandom.utils.pagging.GifsDataSource import com.haretskiy.pavel.gifrandom.utils.pagging.GifsSourceFactory import java.util.concurrent.Executors - class MainViewModel(private val context: Application, - private val factory: GifsSourceFactory) : AndroidViewModel(context) { - + private val factory: GifsSourceFactory, + private val connectivity: Connectivity) : AndroidViewModel(context) { + private val config = PagedList.Config.Builder() .setEnablePlaceholders(false) .setInitialLoadSizeHint(INITIAL_LOAD_SIZE) .setPrefetchDistance(PREFETCH_SIZE) .setPageSize(LIMIT) .build() - - val pagedListLiveData = LivePagedListBuilder( - factory.initCallback(object : GifsDataSource.GifsLoadedCallback { + + val pagedListLiveData = + LivePagedListBuilder(factory.initCallback(object : GifsDataSource.GifsLoadedCallback { override fun onStartInitialLoad() { progress.set(View.VISIBLE) } - + override fun onFinishInitialLoad() { progress.set(View.GONE) } - }), config) - .setFetchExecutor(Executors.newSingleThreadExecutor()) - .build() - + }), config).setFetchExecutor(Executors.newSingleThreadExecutor()) + .build() + val searchWord: ObservableField = ObservableField() val ratingSelectedPos = ObservableInt(ZERO) val progress = ObservableInt(View.VISIBLE) val toolbarVisibility = ObservableInt(View.VISIBLE) - + val positLiveData = MutableLiveData() - + private fun getCurrentRating(): String { val ratings = context.resources.getStringArray(R.array.ratings) return ratings[ratingSelectedPos.get()] } - + fun onClickSearch(@Suppress("UNUSED_PARAMETER") v: View) { factory.rating = getCurrentRating() factory.word = searchWord.get() ?: EMPTY_STRING factory.invalidate() } - + fun onClickFilter(@Suppress("UNUSED_PARAMETER") v: View) { positLiveData.postValue(true) } - - + + fun observeOnline() = connectivity.onlineChanges() + @Suppress("UNUSED_PARAMETER") fun onRatingSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + //not implemented } - + fun setBinding(binding: ActivityMainBinding) { binding.appbar.addOnOffsetChangedListener { appBarLayout, verticalOffset -> if (Math.abs(verticalOffset) - appBarLayout.totalScrollRange >= 0) { @@ -77,6 +79,6 @@ class MainViewModel(private val context: Application, } } } - + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae936e6..d955ed0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,8 @@ GifRandom Search + Online + Offline Y G From 622c645d1887e9bdee612a425262a109c737a793 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sun, 10 Feb 2019 13:02:47 +0300 Subject: [PATCH 04/11] minor changes --- .../main/java/com/haretskiy/pavel/gifrandom/Constants.kt | 3 +-- .../java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt | 6 +++--- .../haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt index df5965b..8284dc1 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt @@ -1,6 +1,5 @@ package com.haretskiy.pavel.gifrandom - const val START = "START ->" const val END = "END <-" const val RESPONSE = "RESPONSE" @@ -11,8 +10,8 @@ const val ZERO = 0 const val BASE_URL = "https://api.giphy.com/v1/" const val API_KEY = "ixOZB0aOMOF7Ivx1vuTIBAeXdksNdGTB" -const val LIMIT = 25 const val INITIAL_LOAD_SIZE = 50 +const val PAGE_SIZE = INITIAL_LOAD_SIZE / 2 const val PREFETCH_SIZE = 10 const val ZERO_OFFSET = "0" diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt index bc45af9..60db493 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt @@ -1,12 +1,12 @@ package com.haretskiy.pavel.gifrandom.rest -import com.haretskiy.pavel.gifrandom.LIMIT +import com.haretskiy.pavel.gifrandom.PAGE_SIZE import com.haretskiy.pavel.gifrandom.ZERO_OFFSET class RestApiImpl(private val restApi: RestApi) { - fun loadGifsAsync(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsAsync(LIMIT, rating, offset) + fun loadGifsAsync(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsAsync(PAGE_SIZE, rating, offset) - fun loadGifsByWordAsync(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWordAsync(word, LIMIT, rating, offset) + fun loadGifsByWordAsync(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWordAsync(word, PAGE_SIZE, rating, offset) } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt index 49d7987..2019ce5 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt @@ -24,7 +24,7 @@ class MainViewModel(private val context: Application, .setEnablePlaceholders(false) .setInitialLoadSizeHint(INITIAL_LOAD_SIZE) .setPrefetchDistance(PREFETCH_SIZE) - .setPageSize(LIMIT) + .setPageSize(PAGE_SIZE) .build() val pagedListLiveData = From 8c7427b91a8008d4ed60a23269daf1c684d996ca Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sun, 10 Feb 2019 13:04:59 +0300 Subject: [PATCH 05/11] minor changes --- .../com/haretskiy/pavel/gifrandom/di/Modules.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt index 7d2f546..5791635 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt @@ -24,7 +24,7 @@ import org.koin.dsl.module.module import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -val restModule: Module = module(definition = { +val restModule: Module = module { single { OkHttpClient.Builder() .addInterceptor(JsonInterceptor()) @@ -48,9 +48,9 @@ val restModule: Module = module(definition = { single { Connectivity(androidApplication(), get()) } factory { androidApplication().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } -}) +} -val appModule: Module = module(definition = { +val appModule: Module = module { single { Toaster(androidApplication()) } single { ImageLoaderImpl() as ImageLoader } single { RouterImpl(androidApplication()) as Router } @@ -58,14 +58,14 @@ val appModule: Module = module(definition = { single { DiffCallBack() } single { GifsSourceFactory(get()) } factory { GifAdapter(get(), get(), get()) } -}) +} -val viewModelModule: Module = module(definition = { +val viewModelModule: Module = module { viewModel { MainViewModel(androidApplication(), get(), get()) } - viewModel {parameterList -> + viewModel { parameterList -> DetailViewModel(get(), parameterList[0]) } -}) +} val modules = listOf(restModule, appModule, viewModelModule) From f94046454ac1b4fc64ce90fec1f11dbfe13ce4a6 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sun, 10 Feb 2019 13:10:55 +0300 Subject: [PATCH 06/11] minor changes --- .../haretskiy/pavel/gifrandom/utils/Connectivity.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt index 89957f8..aedb112 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt @@ -8,6 +8,9 @@ import android.net.NetworkInfo import android.util.Log import com.haretskiy.pavel.gifrandom.distinctUntilChanged import com.haretskiy.pavel.gifrandom.map +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext class Connectivity(context: Context, private val manager: ConnectivityManager) { @@ -47,6 +50,12 @@ class Connectivity(context: Context, private val manager: ConnectivityManager) { return false } - fun onlineChanges() = connectivityLiveData.map { isOnline() }.distinctUntilChanged() + private fun isOnlineAsync(): Boolean { + return runBlocking { + withContext(Dispatchers.Default) { isOnline() } + } + } + + fun onlineChanges() = connectivityLiveData.map { isOnlineAsync() }.distinctUntilChanged() } \ No newline at end of file From 626023181ab17d9f727caba290aeeff5156136b6 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sun, 10 Feb 2019 13:31:52 +0300 Subject: [PATCH 07/11] minor changes --- app/proguard-rules.pro | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 30e1c20..af4bfb9 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -58,10 +58,20 @@ # Retain declared checked exceptions for use by a Proxy instance. -keepattributes Exceptions - +#OkHttp -keepattributes Signature -keepattributes Annotation -keep class okhttp3.** { *; } -keep interface okhttp3.** { *; } -dontwarn okhttp3.** --dontwarn okio.** \ No newline at end of file +-dontwarn okio.** + +#Coroutines +# ServiceLoader support + -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} + +# Most of volatile fields are updated with AFU and should not be mangled +-keepclassmembernames class kotlinx.** { + volatile ; +} \ No newline at end of file From d6d7a8804cfdcc4e3034c87f5850e01a8a732522 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Sun, 10 Feb 2019 22:17:40 +0300 Subject: [PATCH 08/11] minor changes --- .../haretskiy/pavel/gifrandom/activities/MainActivity.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt index 8a44ee3..e176726 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt @@ -5,11 +5,11 @@ import android.arch.paging.PagedList import android.databinding.DataBindingUtil import android.os.Bundle import android.support.v7.app.AppCompatActivity -import android.widget.Toast import com.haretskiy.pavel.gifrandom.R import com.haretskiy.pavel.gifrandom.ZERO import com.haretskiy.pavel.gifrandom.adapters.GifAdapter import com.haretskiy.pavel.gifrandom.databinding.ActivityMainBinding +import com.haretskiy.pavel.gifrandom.utils.Toaster import com.haretskiy.pavel.gifrandom.viewModels.MainViewModel import kotlinx.android.synthetic.main.main_content.* import kotlinx.android.synthetic.main.toolbar.* @@ -22,6 +22,8 @@ class MainActivity : AppCompatActivity() { private val adapter: GifAdapter by inject() + private val toaster: Toaster by inject() + private val binding: ActivityMainBinding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_main) as ActivityMainBinding } @@ -47,9 +49,8 @@ class MainActivity : AppCompatActivity() { }) mainViewModel.observeOnline() .observe(this, Observer { - Toast.makeText(this@MainActivity, if(it == true) getString(R.string.online_state) else getString( - R.string.offline_state), Toast.LENGTH_SHORT) - .show() + toaster.showToast(if (it == true) getString(R.string.online_state) else getString( + R.string.offline_state)) }) rv_gifs.adapter = adapter } From dd5879401760f592e0923a73ebb1b4c07791f5b8 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Tue, 19 Feb 2019 17:43:12 +0300 Subject: [PATCH 09/11] Connectivity works --- .../haretskiy/pavel/gifrandom/di/Modules.kt | 5 +- .../pavel/gifrandom/rest/JsonInterceptor.kt | 56 --------------- .../haretskiy/pavel/gifrandom/rest/RestApi.kt | 7 +- .../pavel/gifrandom/rest/RestApiImpl.kt | 5 +- .../pavel/gifrandom/utils/Connectivity.kt | 69 ++++++++++++++----- .../gifrandom/utils/ConnectivityListener.kt | 47 +++++++++++++ .../gifrandom/utils/ConnectivityLiveData.kt | 67 ------------------ .../gifrandom/viewModels/MainViewModel.kt | 9 +++ 8 files changed, 117 insertions(+), 148 deletions(-) delete mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt create mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt delete mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt index 5791635..e7d8629 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt @@ -7,7 +7,6 @@ import com.haretskiy.pavel.gifrandom.BASE_URL import com.haretskiy.pavel.gifrandom.adapters.GifAdapter import com.haretskiy.pavel.gifrandom.data.Repository import com.haretskiy.pavel.gifrandom.data.RepositoryImpl -import com.haretskiy.pavel.gifrandom.rest.JsonInterceptor import com.haretskiy.pavel.gifrandom.rest.RestApi import com.haretskiy.pavel.gifrandom.rest.RestApiImpl import com.haretskiy.pavel.gifrandom.utils.* @@ -26,9 +25,7 @@ import retrofit2.converter.gson.GsonConverterFactory val restModule: Module = module { single { - OkHttpClient.Builder() - .addInterceptor(JsonInterceptor()) - .build() + OkHttpClient.Builder().build() } single { GsonBuilder().setLenient() diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt deleted file mode 100644 index 61d7ac7..0000000 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.haretskiy.pavel.gifrandom.rest - -import android.util.Log -import com.haretskiy.pavel.gifrandom.* -import okhttp3.Interceptor -import okhttp3.Response -import okhttp3.ResponseBody - - -class JsonInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response? { - val request = chain.request() - try { - val t1 = System.nanoTime() - - val url = request.url().newBuilder().addQueryParameter("apikey", API_KEY).build() - val requestBuilder = request.newBuilder().url(url) - val newRequest = requestBuilder.build() - - Log.d(START, "Sending request ${newRequest.url()} Headers: ${newRequest.headers()}") - - val response = chain.proceed(newRequest) - - val responseBodyString = response.body()?.string() ?: EMPTY_STRING - - val t2 = System.nanoTime() - - val prettyString = responseBodyString.toPrettyFormat() - val length = prettyString.length - if (length > 4000) { - val chunkCount = length / 4000 - for (i in 0..chunkCount) { - val max = 4000 * (i + 1) - if (max >= length) { - Log.d(RESPONSE, "chunk $i of $chunkCount:") - Log.w(RESPONSE, prettyString.substring(4000 * i)) - } else { - Log.d(RESPONSE, "chunk $i of $chunkCount:") - Log.w(RESPONSE, prettyString.substring(4000 * i, max)) - } - } - } else { - Log.w(RESPONSE, prettyString) - } - - Log.d(END, "Received response for ${response.request().url()} for ${(t2 - t1) / 1e6} milliseconds ") - - val responseBody = response.body() - - return response.newBuilder().body(ResponseBody.create(responseBody?.contentType(), responseBodyString.toByteArray())).build() - } catch (ex: Exception) { - return Response.Builder().build() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt index bc61c0f..a4f1913 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt @@ -9,9 +9,10 @@ import retrofit2.http.Query interface RestApi { @GET("gifs/trending") - fun loadGifsAsync(@Query("limit") limit: Int, @Query("rating") rating: String, @Query("offset") offset: String = ZERO_OFFSET): Deferred + fun loadGifsAsync(@Query("apikey") apikey: String, @Query("limit") limit: Int, @Query("rating") rating: String, @Query( + "offset") offset: String = ZERO_OFFSET): Deferred @GET("gifs/search") - fun loadGifsBySearchWordAsync(@Query("q") searchWord: String, @Query("limit") limit: Int, @Query("rating") rating: String, @Query( - "offset") offset: String = ZERO_OFFSET): Deferred + fun loadGifsBySearchWordAsync(@Query("apikey") apikey: String, @Query("q") searchWord: String, @Query( + "limit") limit: Int, @Query("rating") rating: String, @Query("offset") offset: String = ZERO_OFFSET): Deferred } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt index 60db493..560346a 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt @@ -1,12 +1,13 @@ package com.haretskiy.pavel.gifrandom.rest +import com.haretskiy.pavel.gifrandom.API_KEY import com.haretskiy.pavel.gifrandom.PAGE_SIZE import com.haretskiy.pavel.gifrandom.ZERO_OFFSET class RestApiImpl(private val restApi: RestApi) { - fun loadGifsAsync(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsAsync(PAGE_SIZE, rating, offset) + fun loadGifsAsync(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsAsync(API_KEY, PAGE_SIZE, rating, offset) - fun loadGifsByWordAsync(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWordAsync(word, PAGE_SIZE, rating, offset) + fun loadGifsByWordAsync(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWordAsync(API_KEY, word, PAGE_SIZE, rating, offset) } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt index aedb112..380fa8b 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt @@ -1,31 +1,62 @@ package com.haretskiy.pavel.gifrandom.utils -import android.annotation.SuppressLint +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData import android.content.Context +import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager import android.net.NetworkInfo import android.util.Log import com.haretskiy.pavel.gifrandom.distinctUntilChanged -import com.haretskiy.pavel.gifrandom.map -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.actor +import kotlin.coroutines.CoroutineContext -class Connectivity(context: Context, private val manager: ConnectivityManager) { +class Connectivity(context: Context, private val manager: ConnectivityManager) : CoroutineScope { - private val connectivityLiveData = ConnectivityLiveData(context, + private var isStarted = false + + private val job: Job = Job() + + override val coroutineContext: CoroutineContext + get() = job + + private val onlineData = MutableLiveData() + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.e("Error", "Caught $exception") + exception.printStackTrace() + } + + private val connectActor = actor(Dispatchers.Default + handler) { + for (intents in channel) { + onlineData.postValue(isOnlineAsync()) + } + } + + fun start() { + if (!isStarted) { + connectivityListener.start() + isStarted = true + } + } + + fun stop() { + if (isStarted) { + connectivityListener.stop() + isStarted = false + job.cancel() + } + } + + private val connectivityListener = ConnectivityListener(context, + connectActor, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION), null, null) - companion object { - const val TAG = "Connectivity" - } - - @Suppress("DEPRECATION") - @SuppressLint("MissingPermission") - fun isOnline(): Boolean { + private fun isOnline(): Boolean { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { val activeNetworks = manager.allNetworks for (network in activeNetworks) { @@ -52,10 +83,16 @@ class Connectivity(context: Context, private val manager: ConnectivityManager) { private fun isOnlineAsync(): Boolean { return runBlocking { - withContext(Dispatchers.Default) { isOnline() } + withContext(Dispatchers.Default + handler) { isOnline() } } } - fun onlineChanges() = connectivityLiveData.map { isOnlineAsync() }.distinctUntilChanged() + fun onlineChanges(): LiveData { + return onlineData.distinctUntilChanged() + } + + companion object { + const val TAG = "Connectivity" + } } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt new file mode 100644 index 0000000..818f0fc --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt @@ -0,0 +1,47 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.launch + +class ConnectivityListener(private val context: Context, + private val actor: SendChannel, + private val intentFilter: IntentFilter, + private val broadcastPermission: String?, + private val schedulerHandler: Handler?) { + + private lateinit var broadcastReceiver: BroadcastReceiver + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.e("Error", "Caught $exception") + exception.printStackTrace() + } + + fun start() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + GlobalScope.launch(handler) { + actor.send(intent) + } + } + } + + context.registerReceiver(broadcastReceiver, + intentFilter, + broadcastPermission, + schedulerHandler) + } + + fun stop() { + GlobalScope.launch(handler) { + context.unregisterReceiver(broadcastReceiver) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt deleted file mode 100644 index d614a55..0000000 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityLiveData.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.haretskiy.pavel.gifrandom.utils - -import android.arch.lifecycle.LifecycleOwner -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.Observer -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Handler -import android.util.Log -import com.haretskiy.pavel.gifrandom.utils.Connectivity.Companion.TAG - -class ConnectivityLiveData(private val context: Context, - private val intentFilter: IntentFilter, - private val broadcastPermission: String?, - private val schedulerHandler: Handler?) : LiveData() { - - private lateinit var broadcastReceiver: BroadcastReceiver - - override fun observe(owner: LifecycleOwner, observer: Observer) { - super.observe(owner, observer) - - observe() - } - - override fun observeForever(observer: Observer) { - super.observeForever(observer) - - observe() - } - - private fun observe() { - try { - broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - postValue(intent) - } - } - - context.registerReceiver(broadcastReceiver, - intentFilter, - broadcastPermission, - schedulerHandler) - } catch (t: Throwable) { - Log.e(TAG, t.toString()) - } - } - - private fun removeObserver() { - try { - context.unregisterReceiver(broadcastReceiver) - } catch (t: Throwable) { - Log.e(TAG, t.toString()) - } - } - - override fun removeObserver(observer: Observer) { - super.removeObserver(observer) - removeObserver() - } - - override fun removeObservers(owner: LifecycleOwner) { - super.removeObservers(owner) - removeObserver() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt index 2019ce5..e63ceeb 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt @@ -46,6 +46,15 @@ class MainViewModel(private val context: Application, val positLiveData = MutableLiveData() + init { + connectivity.start() + } + + override fun onCleared() { + connectivity.stop() + super.onCleared() + } + private fun getCurrentRating(): String { val ratings = context.resources.getStringArray(R.array.ratings) return ratings[ratingSelectedPos.get()] From 95e57f31a1e9d9fe6e06ec53efc8f5d7001b9b32 Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Tue, 19 Feb 2019 17:56:01 +0300 Subject: [PATCH 10/11] handled error --- .../pavel/gifrandom/data/RepositoryImpl.kt | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt index 3f1a64d..d8e671d 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt @@ -5,6 +5,7 @@ import com.haretskiy.pavel.gifrandom.EMPTY_STRING import com.haretskiy.pavel.gifrandom.models.GifResponse import com.haretskiy.pavel.gifrandom.rest.RestApiImpl import kotlinx.coroutines.* +import java.net.ConnectException class RepositoryImpl(private val restApi: RestApiImpl) : Repository { @@ -18,20 +19,26 @@ class RepositoryImpl(private val restApi: RestApiImpl) : Repository { rating, offset)) - private fun load(deferred: Deferred): List = runBlocking { - GlobalScope.async(CoroutineExceptionHandler { _, exception -> - Log.d("RepositoryImpl", "Caught $exception") - }, CoroutineStart.DEFAULT, null, { + private val handler = CoroutineExceptionHandler { _, exception -> + Log.d("RepositoryImpl", "Caught $exception") + exception.printStackTrace() + } + + private fun load(deferred: Deferred): List = runBlocking(Dispatchers.Default + handler) { + try { val responseData = deferred.await() Log.d("RepositoryImpl", "On success") - convertData(responseData) - }) - .await() + convertDataAsync(responseData).await() + } catch (ex: ConnectException) { + Log.d("RepositoryImpl", "On error$ex") + emptyList() + } } - private suspend fun convertData(responseData: GifResponse) = GlobalScope.async(Dispatchers.Default, - CoroutineStart.DEFAULT, - null, - { responseData.data.map { it.images?.original?.url ?: EMPTY_STRING } }).await() + private fun convertDataAsync(responseData: GifResponse) = GlobalScope.async(Dispatchers.Default + handler) { + responseData.data.map { + it.images?.original?.url ?: EMPTY_STRING + } + } } \ No newline at end of file From daff522ea71fd66e06bbae8605d510afd53e911b Mon Sep 17 00:00:00 2001 From: Pavel Haretskiy Date: Wed, 20 Feb 2019 12:55:36 +0300 Subject: [PATCH 11/11] ActivityCounter added --- .../java/com/haretskiy/pavel/gifrandom/App.kt | 9 +- .../haretskiy/pavel/gifrandom/Constants.kt | 6 +- .../gifrandom/activities/DetailActivity.kt | 21 ++-- .../pavel/gifrandom/adapters/GifAdapter.kt | 3 +- .../haretskiy/pavel/gifrandom/di/Modules.kt | 5 +- .../{utils => }/pagging/DiffCalback.kt | 2 +- .../{utils => }/pagging/GifsDataSource.kt | 2 +- .../{utils => }/pagging/GifsSourceFactory.kt | 2 +- .../pavel/gifrandom/utils/ActivityCounter.kt | 103 ++++++++++++++++++ .../utils/SimpleActivityLifecycleCallbacks.kt | 29 +++++ .../gifrandom/viewModels/DetailViewModel.kt | 4 + .../gifrandom/viewModels/MainViewModel.kt | 4 +- 12 files changed, 164 insertions(+), 26 deletions(-) rename app/src/main/java/com/haretskiy/pavel/gifrandom/{utils => }/pagging/DiffCalback.kt (85%) rename app/src/main/java/com/haretskiy/pavel/gifrandom/{utils => }/pagging/GifsDataSource.kt (97%) rename app/src/main/java/com/haretskiy/pavel/gifrandom/{utils => }/pagging/GifsSourceFactory.kt (94%) create mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt create mode 100644 app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/App.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/App.kt index 4ed8cd1..0c3e254 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/App.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/App.kt @@ -2,14 +2,17 @@ package com.haretskiy.pavel.gifrandom import android.app.Application import com.haretskiy.pavel.gifrandom.di.modules +import com.haretskiy.pavel.gifrandom.utils.ActivityCounter import org.koin.android.ext.android.startKoin - class App : Application() { - + override fun onCreate() { super.onCreate() + startKoin(this, modules) + + ActivityCounter.init(this) } - + } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt index 8284dc1..896d952 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/Constants.kt @@ -1,9 +1,5 @@ package com.haretskiy.pavel.gifrandom -const val START = "START ->" -const val END = "END <-" -const val RESPONSE = "RESPONSE" - const val EMPTY_STRING = "" const val DEFAULT_RATING = "Y" const val ZERO = 0 @@ -18,6 +14,6 @@ const val ZERO_OFFSET = "0" const val BUNDLE_KEY_URL_DETAIL = "BUNDLE_KEY_URL_DETAIL" const val VIEW_NAME_IMAGE = "VIEW_NAME_IMAGE" -const val START_ANIMATION_DELAY = 600L +const val START_ANIMATION_DELAY = 1000L const val SHARE_TYPE_TEXT = "text/plain" \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt index 86d2778..a2c84f8 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt @@ -12,28 +12,31 @@ import org.koin.android.viewmodel.ext.android.viewModel import org.koin.core.parameter.ParameterList class DetailActivity : AppCompatActivity() { - + private var url = EMPTY_STRING - + private val handler = Handler() + private val detailViewModel: DetailViewModel by viewModel { ParameterList(url) } - + private val binding: ActivityDetailBinding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_detail) as ActivityDetailBinding } - + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + url = intent.getStringExtra(BUNDLE_KEY_URL_DETAIL) - + binding.detailModel = detailViewModel detailViewModel.initObservers(this) - + makeTransition() } - + private fun makeTransition() { ViewCompat.setTransitionName(binding.imageView, VIEW_NAME_IMAGE) - Handler().postDelayed({ recreate() }, START_ANIMATION_DELAY) + handler.postDelayed({ + detailViewModel.invalidate() + }, START_ANIMATION_DELAY) } } diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt index ea996e4..5f3d0ac 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt @@ -8,12 +8,11 @@ import android.view.ViewGroup import com.haretskiy.pavel.gifrandom.EMPTY_STRING import com.haretskiy.pavel.gifrandom.R import com.haretskiy.pavel.gifrandom.databinding.ItemHolderBinding +import com.haretskiy.pavel.gifrandom.pagging.DiffCallBack import com.haretskiy.pavel.gifrandom.utils.ImageLoader import com.haretskiy.pavel.gifrandom.utils.Router -import com.haretskiy.pavel.gifrandom.utils.pagging.DiffCallBack import com.haretskiy.pavel.gifrandom.views.GifHolder - class GifAdapter(diffCallback: DiffCallBack, private val imageLoader: ImageLoader, private val router: Router) : PagedListAdapter(diffCallback) { diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt index e7d8629..9433382 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt @@ -7,11 +7,11 @@ import com.haretskiy.pavel.gifrandom.BASE_URL import com.haretskiy.pavel.gifrandom.adapters.GifAdapter import com.haretskiy.pavel.gifrandom.data.Repository import com.haretskiy.pavel.gifrandom.data.RepositoryImpl +import com.haretskiy.pavel.gifrandom.pagging.DiffCallBack +import com.haretskiy.pavel.gifrandom.pagging.GifsSourceFactory import com.haretskiy.pavel.gifrandom.rest.RestApi import com.haretskiy.pavel.gifrandom.rest.RestApiImpl import com.haretskiy.pavel.gifrandom.utils.* -import com.haretskiy.pavel.gifrandom.utils.pagging.DiffCallBack -import com.haretskiy.pavel.gifrandom.utils.pagging.GifsSourceFactory import com.haretskiy.pavel.gifrandom.viewModels.DetailViewModel import com.haretskiy.pavel.gifrandom.viewModels.MainViewModel import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory @@ -54,6 +54,7 @@ val appModule: Module = module { single { RepositoryImpl(get()) as Repository } single { DiffCallBack() } single { GifsSourceFactory(get()) } + factory { GifAdapter(get(), get(), get()) } } diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/DiffCalback.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/DiffCalback.kt similarity index 85% rename from app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/DiffCalback.kt rename to app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/DiffCalback.kt index 3e63d02..0d74d6e 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/DiffCalback.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/DiffCalback.kt @@ -1,4 +1,4 @@ -package com.haretskiy.pavel.gifrandom.utils.pagging +package com.haretskiy.pavel.gifrandom.pagging import android.support.v7.util.DiffUtil diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsDataSource.kt similarity index 97% rename from app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt rename to app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsDataSource.kt index 337d629..b545c57 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsDataSource.kt @@ -1,4 +1,4 @@ -package com.haretskiy.pavel.gifrandom.utils.pagging +package com.haretskiy.pavel.gifrandom.pagging import android.arch.paging.PositionalDataSource import com.haretskiy.pavel.gifrandom.ZERO_OFFSET diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsSourceFactory.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsSourceFactory.kt similarity index 94% rename from app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsSourceFactory.kt rename to app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsSourceFactory.kt index 91eb43b..a50f560 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsSourceFactory.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsSourceFactory.kt @@ -1,4 +1,4 @@ -package com.haretskiy.pavel.gifrandom.utils.pagging +package com.haretskiy.pavel.gifrandom.pagging import android.arch.paging.DataSource import com.haretskiy.pavel.gifrandom.DEFAULT_RATING diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt new file mode 100644 index 0000000..60350d5 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt @@ -0,0 +1,103 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.util.Log +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.actor +import kotlin.coroutines.CoroutineContext + +object ActivityCounter : CoroutineScope { + + private val job = Job() + + private val actionStore = HashMap, (Count) -> Unit>() + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.e("Error", "Caught $exception") + exception.printStackTrace() + } + + override val coroutineContext: CoroutineContext + get() = job + handler + + private var counter: Int = 0 + + private val connectActor = actor(Dispatchers.Default) { + var prevCount: Count? = null + for (count in channel) { + if (count != prevCount) { + for ((_, action) in actionStore) { + GlobalScope.launch(Dispatchers.Default) { + action.invoke(count) + } + } + prevCount = count + + Log.d("ActivityCounter", + "Activities: old count =${count.previousValue}, new count =${count.newValue}") + } + } + } + + fun addAction(clazz: Class, action: (count: Count) -> Unit) { + actionStore[clazz] = action + } + + fun removeAction(clazz: Class) { + actionStore.remove(clazz) + } + + fun init(app: Application) { + app.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallbacks() { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + + val count = Count() + count.previousValue = counter + + counter++ + Log.d("ActivityCounter", "onActivityCreated ${activity.javaClass.name} ") + + count.newValue = counter + + GlobalScope.launch { + connectActor.send(count) + } + } + + override fun onActivityDestroyed(activity: Activity) { + + val count = Count() + count.previousValue = counter + + counter-- + Log.d("ActivityCounter", "onActivityDestroyed ${activity.javaClass.name} ") + + count.newValue = counter + GlobalScope.launch { + connectActor.send(count) + } + if (count.newValue == 0) { + Log.d("ActivityCounter", "0 activities, Cancel job") + } + } + }) + } + + class Count { + var newValue: Int = 0 + var previousValue: Int = 0 + + override fun equals(other: Any?): Boolean { + return (other is Count) && other.newValue == newValue && other.previousValue == previousValue + } + + override fun hashCode(): Int { + var result = newValue + result = 31 * result + previousValue + return result + } + } +} diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt new file mode 100644 index 0000000..55ce101 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt @@ -0,0 +1,29 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +open class SimpleActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { + } + + override fun onActivityDestroyed(activity: Activity) { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt index 3d9c097..b089b06 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt @@ -25,4 +25,8 @@ class DetailViewModel(imageLoader: ImageLoader, progress.set(if (it == true) View.VISIBLE else View.GONE) }) } + + fun invalidate() { + url.notifyChange() + } } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt index e63ceeb..dc935cc 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt @@ -11,9 +11,9 @@ import android.view.View import android.widget.AdapterView import com.haretskiy.pavel.gifrandom.* import com.haretskiy.pavel.gifrandom.databinding.ActivityMainBinding +import com.haretskiy.pavel.gifrandom.pagging.GifsDataSource +import com.haretskiy.pavel.gifrandom.pagging.GifsSourceFactory import com.haretskiy.pavel.gifrandom.utils.Connectivity -import com.haretskiy.pavel.gifrandom.utils.pagging.GifsDataSource -import com.haretskiy.pavel.gifrandom.utils.pagging.GifsSourceFactory import java.util.concurrent.Executors class MainViewModel(private val context: Application,