mirror of
https://github.com/hmalik144/Android-developer-tech-test---POQ.git
synced 2025-12-10 03:05:24 +00:00
MVVM implementation
This commit is contained in:
112
.idea/codeStyles/Project.xml
generated
112
.idea/codeStyles/Project.xml
generated
@@ -1,8 +1,120 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<AndroidXmlCodeStyleSettings>
|
||||||
|
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||||
|
</AndroidXmlCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
<codeStyleSettings language="kotlin">
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
|
|||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
|||||||
<component name="Kotlin2JsCompilerArguments">
|
<component name="Kotlin2JsCompilerArguments">
|
||||||
<option name="sourceMapEmbedSources" />
|
<option name="sourceMapEmbedSources" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ apply plugin: 'kotlin-android'
|
|||||||
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
//kotlin kapt and navigation safeargs plugin
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.example.h_mal.myapplication"
|
applicationId "com.example.h_mal.myapplication"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -20,21 +23,50 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dataBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
implementation 'androidx.core:core-ktx:1.1.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
androidTestImplementation 'com.android.support.test:rules:1.0.2'
|
|
||||||
implementation 'com.android.support:cardview-v7:28.0.0'
|
//Retrofit and GSON
|
||||||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
|
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
|
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
|
//Kotlin Coroutines
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||||
|
|
||||||
|
// ViewModel and LiveData
|
||||||
|
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
|
||||||
|
|
||||||
|
//New Material Design
|
||||||
|
implementation 'com.google.android.material:material:1.1.0-alpha10'
|
||||||
|
|
||||||
|
//Kodein Dependency Injection
|
||||||
|
implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1"
|
||||||
|
implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1"
|
||||||
|
|
||||||
|
//Android Room
|
||||||
|
implementation "androidx.room:room-runtime:2.2.0-rc01"
|
||||||
|
implementation "androidx.room:room-ktx:2.2.0-rc01"
|
||||||
|
kapt "androidx.room:room-compiler:2.2.0-rc01"
|
||||||
|
|
||||||
|
implementation 'com.xwray:groupie:2.3.0'
|
||||||
|
implementation 'com.xwray:groupie-kotlin-android-extensions:2.3.0'
|
||||||
|
implementation 'com.xwray:groupie-databinding:2.3.0'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.example.h_mal.myapplication.ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.test.filters.LargeTest
|
||||||
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MainActivityTest {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".AppClass"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
package com.example.h_mal.myapplication.Api
|
package com.example.h_mal.myapplication.Api
|
||||||
|
|
||||||
import com.example.h_mal.myapplication.model.Repo
|
import com.example.h_mal.myapplication.model.Repo
|
||||||
import io.reactivex.Observable
|
import okhttp3.OkHttpClient
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
|
||||||
interface GetData{
|
interface GetData{
|
||||||
|
|
||||||
|
companion object{
|
||||||
|
operator fun invoke(
|
||||||
|
networkConnectionInterceptor: NetworkConnectionInterceptor
|
||||||
|
) : GetData{
|
||||||
|
|
||||||
|
val okkHttpclient = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(networkConnectionInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.client(okkHttpclient)
|
||||||
|
.baseUrl("https://api.github.com/orgs/square/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(GetData::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET("repos")
|
@GET("repos")
|
||||||
fun getData() : Observable<List<Repo>>
|
suspend fun getData() : Response<List<Repo>>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.example.h_mal.myapplication.Api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NetworkConnectionInterceptor(
|
||||||
|
context: Context
|
||||||
|
): Interceptor {
|
||||||
|
|
||||||
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
if (!isInternetAvailable())
|
||||||
|
throw IOException("Make sure you have an active data connection")
|
||||||
|
return chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isInternetAvailable(): Boolean {
|
||||||
|
var result = false
|
||||||
|
val connectivityManager =
|
||||||
|
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
|
||||||
|
connectivityManager?.let {
|
||||||
|
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
|
||||||
|
result = when {
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.example.h_mal.myapplication.Api
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
abstract class SafeApiRequest {
|
||||||
|
|
||||||
|
suspend fun<T: Any> apiRequest(call: suspend () -> Response<T>) : T{
|
||||||
|
val response = call.invoke()
|
||||||
|
if(response.isSuccessful){
|
||||||
|
return response.body()!!
|
||||||
|
}else{
|
||||||
|
val error = response.errorBody()?.string()
|
||||||
|
|
||||||
|
val message = StringBuilder()
|
||||||
|
error?.let{
|
||||||
|
try{
|
||||||
|
message.append(JSONObject(it).getString("message"))
|
||||||
|
}catch(e: JSONException){ }
|
||||||
|
message.append("\n")
|
||||||
|
}
|
||||||
|
message.append("Error Code: ${response.code()}")
|
||||||
|
throw IOException(message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.example.h_mal.myapplication
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.example.h_mal.myapplication.Api.GetData
|
||||||
|
import com.example.h_mal.myapplication.Api.NetworkConnectionInterceptor
|
||||||
|
import com.example.h_mal.myapplication.db.AppDatabase
|
||||||
|
import com.example.h_mal.myapplication.repository.Repository
|
||||||
|
import com.example.h_mal.myapplication.ui.DefaultViewFactory
|
||||||
|
import org.kodein.di.Kodein
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.x.androidXModule
|
||||||
|
import org.kodein.di.generic.bind
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
import org.kodein.di.generic.provider
|
||||||
|
import org.kodein.di.generic.singleton
|
||||||
|
|
||||||
|
class AppClass : Application(), KodeinAware{
|
||||||
|
|
||||||
|
|
||||||
|
override val kodein
|
||||||
|
= Kodein.lazy{ import(androidXModule(this@AppClass))
|
||||||
|
|
||||||
|
bind() from singleton { NetworkConnectionInterceptor(instance()) }
|
||||||
|
bind() from singleton { GetData(instance()) }
|
||||||
|
bind() from singleton { AppDatabase(instance()) }
|
||||||
|
bind() from singleton {
|
||||||
|
Repository(
|
||||||
|
instance(),
|
||||||
|
instance()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
bind() from provider { DefaultViewFactory(instance()) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.example.h_mal.myapplication.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.example.h_mal.myapplication.model.Repo
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [Repo::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun getRepoDao() : RepoDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: AppDatabase? = null
|
||||||
|
private val LOCK = Any()
|
||||||
|
|
||||||
|
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
|
||||||
|
instance ?: buildDatabase(context).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDatabase(context: Context) =
|
||||||
|
Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
"MyDatabase.db"
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.h_mal.myapplication.db
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.example.h_mal.myapplication.model.Repo
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RepoDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun saveAllRepos(repos : List<Repo>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Repo")
|
||||||
|
fun getRepos() : LiveData<List<Repo>>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
package com.example.h_mal.myapplication.model
|
package com.example.h_mal.myapplication.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
@Entity
|
||||||
data class Repo(
|
data class Repo(
|
||||||
|
@PrimaryKey
|
||||||
|
var id: Int? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var description : String? = null,
|
var description : String? = null,
|
||||||
var language : String? = null,
|
var language : String? = null,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.example.h_mal.myapplication.repository
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.example.h_mal.myapplication.Api.GetData
|
||||||
|
import com.example.h_mal.myapplication.Api.SafeApiRequest
|
||||||
|
import com.example.h_mal.myapplication.db.AppDatabase
|
||||||
|
import com.example.h_mal.myapplication.model.Repo
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
class Repository(
|
||||||
|
private val api: GetData,
|
||||||
|
private val db: AppDatabase
|
||||||
|
):SafeApiRequest(){
|
||||||
|
|
||||||
|
private val repos = MutableLiveData<List<Repo>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
repos.observeForever {
|
||||||
|
saveRepos(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRepos(): LiveData<List<Repo>> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
fetchRepos()
|
||||||
|
db.getRepoDao().getRepos()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchRepos() {
|
||||||
|
try {
|
||||||
|
val response = apiRequest { api.getData() }
|
||||||
|
repos.postValue(response)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun saveRepos(repos: List<Repo>) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch{
|
||||||
|
db.getRepoDao().saveAllRepos(repos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.example.h_mal.myapplication.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.example.h_mal.myapplication.repository.Repository
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class DefaultViewFactory(
|
||||||
|
private val repository: Repository
|
||||||
|
) : ViewModelProvider.NewInstanceFactory(){
|
||||||
|
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return MainViewModel(repository) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.h_mal.myapplication.ui
|
package com.example.h_mal.myapplication.ui
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
@@ -50,7 +51,9 @@ class ListViewAdapter(context: Context, objects: MutableList<Repo>) :
|
|||||||
|
|
||||||
//language text and corresponding colour according to github is applied
|
//language text and corresponding colour according to github is applied
|
||||||
view.lang?.text = item.language
|
view.lang?.text = item.language
|
||||||
getColor(item.language)?.let { it1 -> view.lang_col.setCardBackgroundColor(it1) }
|
|
||||||
|
|
||||||
|
getColor(item.language)?.let { it1 -> view.lang_col.setBackgroundColor(it1) }
|
||||||
}else{
|
}else{
|
||||||
//language was null therefore view to be hidden
|
//language was null therefore view to be hidden
|
||||||
view.lang_layout.visibility = View.GONE
|
view.lang_layout.visibility = View.GONE
|
||||||
|
|||||||
@@ -1,46 +1,77 @@
|
|||||||
package com.example.h_mal.myapplication.ui
|
package com.example.h_mal.myapplication.ui
|
||||||
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
import androidx.databinding.DataBindingUtil
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.widget.SwipeRefreshLayout
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.SearchView
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.example.h_mal.myapplication.Api.GetData
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.example.h_mal.myapplication.R
|
import com.example.h_mal.myapplication.R
|
||||||
import com.example.h_mal.myapplication.model.Repo
|
import com.example.h_mal.myapplication.databinding.ActivityMainBinding
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import kotlinx.coroutines.Dispatchers
|
||||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.Retrofit
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.kodein
|
||||||
import io.reactivex.schedulers.Schedulers
|
import org.kodein.di.generic.instance
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.observers.DisposableObserver
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity(), KodeinAware {
|
||||||
lateinit var searchView: SearchView
|
lateinit var searchView: SearchView
|
||||||
lateinit var myCompositeDisposable: CompositeDisposable
|
lateinit var adapterLV: ListViewAdapter
|
||||||
|
|
||||||
val urlString = "https://api.github.com/orgs/square/"
|
override val kodein by kodein()
|
||||||
|
|
||||||
|
private val factory: DefaultViewFactory by instance()
|
||||||
|
|
||||||
|
private lateinit var viewModel: MainViewModel
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
spinner.visibility = View.VISIBLE
|
viewModel = ViewModelProviders.of(this,factory).get(MainViewModel::class.java)
|
||||||
|
|
||||||
myCompositeDisposable = CompositeDisposable()
|
|
||||||
|
|
||||||
//begin populating list
|
|
||||||
loadData()
|
|
||||||
|
|
||||||
|
bindUI()
|
||||||
//set a listener for the swipe to refresh
|
//set a listener for the swipe to refresh
|
||||||
swipe_refresh.setOnRefreshListener(swipeRefreshListener)
|
swipe_refresh.setOnRefreshListener(swipeRefreshListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bindUI() = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
spinner.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
viewModel.repos.await().observe(this@MainActivity, Observer {
|
||||||
|
spinner.visibility = View.GONE
|
||||||
|
|
||||||
|
if (it.isEmpty()){
|
||||||
|
empty_view.visibility = View.VISIBLE
|
||||||
|
searchView.setOnQueryTextListener(null)
|
||||||
|
}else{
|
||||||
|
empty_view.visibility = View.GONE
|
||||||
|
|
||||||
|
adapterLV = ListViewAdapter(baseContext, it.toMutableList())
|
||||||
|
//apply adapter to listview
|
||||||
|
list_view.adapter = adapterLV
|
||||||
|
|
||||||
|
list_view.setOnItemClickListener { parent, view, position, id ->
|
||||||
|
adapterLV.openLink(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
//search view has its query change listener applied
|
||||||
|
searchView.setOnQueryTextListener(queryListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//implement search interface in the menu
|
//implement search interface in the menu
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
//inflate custom menu as our menu
|
//inflate custom menu as our menu
|
||||||
@@ -53,92 +84,27 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
|
|
||||||
myCompositeDisposable?.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
val swipeRefreshListener = SwipeRefreshLayout.OnRefreshListener{
|
val swipeRefreshListener = SwipeRefreshLayout.OnRefreshListener{
|
||||||
//populate list when pulling to refresh
|
//populate list when pulling to refresh
|
||||||
// callData()
|
bindUI()
|
||||||
loadData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadData(){
|
val queryListener = object : SearchView.OnQueryTextListener{
|
||||||
//clear list before populating
|
|
||||||
list_view.adapter = null
|
|
||||||
|
|
||||||
val requestInterface = Retrofit.Builder()
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
.baseUrl(urlString)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
|
||||||
.build().create(GetData::class.java)
|
|
||||||
|
|
||||||
myCompositeDisposable.add(requestInterface.getData()
|
return true
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribeWith(object : DisposableObserver<List<Repo>>() {
|
|
||||||
override fun onNext(t: List<Repo>) {
|
|
||||||
handleResponse(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onComplete() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
handleError()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleResponse(objectList: List<Repo>) {
|
|
||||||
|
|
||||||
spinner.visibility = View.GONE
|
|
||||||
|
|
||||||
//if swipe refresh is refreshing then stop
|
|
||||||
swipe_refresh.isRefreshing = false
|
|
||||||
|
|
||||||
if (objectList.isNotEmpty()){
|
|
||||||
//list is not empty
|
|
||||||
empty_view.visibility = View.GONE
|
|
||||||
//custom list view adapter created
|
|
||||||
val adapterLV = ListViewAdapter(baseContext, objectList.toMutableList())
|
|
||||||
//apply adapter to listview
|
|
||||||
list_view.adapter = adapterLV
|
|
||||||
|
|
||||||
list_view.setOnItemClickListener { parent, view, position, id ->
|
|
||||||
adapterLV.openLink(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
//search view has its query change listener applied
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
//as the test is changed the list is filtered
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
//filter list function
|
|
||||||
adapterLV.filter.filter(newText)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//as the test is changed the list is filtered
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
//filter list function
|
||||||
|
adapterLV.filter.filter(newText)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleError(){
|
|
||||||
//if swipe refresh is refreshing then stop
|
|
||||||
swipe_refresh.isRefreshing = false
|
|
||||||
//list is empty
|
|
||||||
empty_view.visibility = View.VISIBLE
|
|
||||||
//progress bar hidden
|
|
||||||
spinner.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.h_mal.myapplication.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.example.h_mal.myapplication.repository.Repository
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
|
class MainViewModel(val repository : Repository) : ViewModel(){
|
||||||
|
|
||||||
|
val repos by lazy{
|
||||||
|
GlobalScope.async(start = CoroutineStart.LAZY) {
|
||||||
|
repository.getRepos()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<android.support.constraint.ConstraintLayout
|
<layout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.MainActivity">
|
|
||||||
|
|
||||||
<android.support.v4.widget.SwipeRefreshLayout
|
<data>
|
||||||
android:id="@+id/swipe_refresh"
|
<variable
|
||||||
|
name="viewmodel"
|
||||||
|
type="com.example.h_mal.myapplication.ui.MainViewModel" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.MainActivity">
|
||||||
<ListView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/list_view"
|
android:id="@+id/swipe_refresh"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
<ListView
|
||||||
|
android:id="@+id/list_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<RelativeLayout
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
android:id="@+id/empty_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent">
|
|
||||||
|
|
||||||
<TextView
|
<RelativeLayout
|
||||||
android:id="@+id/empty_title_text"
|
android:id="@+id/empty_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerHorizontal="true"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:fontFamily="sans-serif-medium"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:paddingTop="16dp"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:text="list empty"
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
android:textAppearance="?android:textAppearanceMedium"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/empty_subtitle_text"
|
android:id="@+id/empty_title_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:text="list empty"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_subtitle_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/empty_title_text"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:fontFamily="sans-serif"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:text="please check connection"
|
||||||
|
android:textAppearance="?android:textAppearanceSmall"
|
||||||
|
android:textColor="#A2AAB0"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<ProgressBar android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/empty_title_text"
|
android:id="@+id/spinner"
|
||||||
android:layout_centerHorizontal="true"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
android:fontFamily="sans-serif"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:paddingTop="8dp"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:text="please check connection"
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:textColor="#A2AAB0"/>
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/spinner" app:layout_constraintLeft_toLeftOf="parent"
|
</layout>
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
|
||||||
<android.support.v7.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="12dp"
|
android:layout_marginLeft="12dp"
|
||||||
@@ -68,7 +68,7 @@ more text more of a description." />
|
|||||||
android:layout_marginTop="6dp" android:gravity="right">
|
android:layout_marginTop="6dp" android:gravity="right">
|
||||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||||
tools:text="Java" android:gravity="center" android:id="@+id/lang"/>
|
tools:text="Java" android:gravity="center" android:id="@+id/lang"/>
|
||||||
<android.support.v7.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:layout_width="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="16dp"
|
android:layout_height="16dp"
|
||||||
android:layout_marginLeft="4dp"
|
android:layout_marginLeft="4dp"
|
||||||
@@ -79,6 +79,6 @@ more text more of a description." />
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</android.support.v7.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
<item android:id="@+id/search"
|
<item android:id="@+id/search"
|
||||||
android:title="@string/search_title"
|
android:title="@string/search_title"
|
||||||
android:icon="@drawable/ic_search_white_24dp"
|
android:icon="@drawable/ic_search_white_24dp"
|
||||||
app:showAsAction="collapseActionView|ifRoom" app:actionViewClass="android.widget.SearchView"/>
|
app:showAsAction="collapseActionView|ifRoom"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
|
||||||
</menu>
|
</menu>
|
||||||
@@ -8,7 +8,7 @@ buildscript {
|
|||||||
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ org.gradle.jvmargs=-Xmx1536m
|
|||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
|||||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Tue Dec 03 01:40:10 AEDT 2019
|
#Fri Jan 24 13:56:41 UTC 2020
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user