mirror of
https://github.com/hmalik144/Android-developer-tech-test---POQ.git
synced 2025-12-10 03:05:24 +00:00
New commit with added features:
- implement retrofit and rxjava
This commit is contained in:
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
||||
<component name="Kotlin2JsCompilerArguments">
|
||||
<option name="sourceMapEmbedSources" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -30,8 +30,11 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.2.1'
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
androidTestImplementation 'com.android.support.test:rules:1.0.2'
|
||||
implementation 'com.android.support:cardview-v7:28.0.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||
}
|
||||
|
||||
@@ -2,28 +2,22 @@ package com.example.h_mal.myapplication
|
||||
|
||||
|
||||
import android.support.test.espresso.Espresso
|
||||
import android.support.test.espresso.Espresso.*
|
||||
import android.support.test.espresso.action.ViewActions.click
|
||||
import android.support.test.espresso.matcher.ViewMatchers.withId
|
||||
import android.support.test.espresso.Espresso.onView
|
||||
import android.support.test.espresso.assertion.ViewAssertions.matches
|
||||
import android.support.test.espresso.matcher.ViewMatchers.*
|
||||
import android.support.test.filters.LargeTest
|
||||
import android.support.test.rule.ActivityTestRule
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.example.h_mal.myapplication.ui.MainActivity
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.anything
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import android.support.test.espresso.UiController
|
||||
import android.support.test.orchestrator.junit.BundleJUnitUtils.getDescription
|
||||
import android.support.test.espresso.ViewAction
|
||||
import android.support.test.espresso.action.ViewActions.swipeDown
|
||||
import android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast
|
||||
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -36,24 +30,6 @@ class MainActivityTest {
|
||||
@Test
|
||||
fun mainActivityTest() {
|
||||
|
||||
onIdle()
|
||||
|
||||
|
||||
val relativeLayout = onData(anything())
|
||||
.inAdapterView(
|
||||
allOf(
|
||||
withId(R.id.list_view),
|
||||
childAtPosition(
|
||||
withId(R.id.swipe_refresh),
|
||||
0
|
||||
)
|
||||
)
|
||||
)
|
||||
.atPosition(0)
|
||||
relativeLayout.perform(click())
|
||||
|
||||
onView(withId(R.id.swipe_refresh))
|
||||
.perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(85)))
|
||||
}
|
||||
|
||||
private fun childAtPosition(
|
||||
@@ -73,20 +49,4 @@ class MainActivityTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun withCustomConstraints(action: ViewAction, constraints: Matcher<View>): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View> {
|
||||
return constraints
|
||||
}
|
||||
|
||||
override fun getDescription(): String {
|
||||
return action.description
|
||||
}
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
action.perform(uiController, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity">
|
||||
<activity android:name=".ui.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.h_mal.myapplication.Api
|
||||
|
||||
import com.example.h_mal.myapplication.model.Repo
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface GetData{
|
||||
|
||||
@GET("repos")
|
||||
fun getData() : Observable<List<Repo>>
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package com.example.h_mal.myapplication
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.support.v4.widget.SwipeRefreshLayout
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.SearchView
|
||||
import com.example.h_mal.myapplication.model.JsonObject
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
lateinit var searchView: SearchView
|
||||
|
||||
val urlString = "https://api.github.com/orgs/square/repos"
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
//begin populating list
|
||||
executeTask()
|
||||
|
||||
//set a listener for the swipe to refresh
|
||||
swipe_refresh.setOnRefreshListener(swipeRefreshListener)
|
||||
}
|
||||
|
||||
//implement search interface in the menu
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
//inflate custom menu as our menu
|
||||
menuInflater.inflate(R.menu.menu, menu)
|
||||
//extract searchview
|
||||
val searchItem = menu?.findItem(R.id.search)
|
||||
//set searchview globally
|
||||
searchView = searchItem?.actionView as SearchView
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
val swipeRefreshListener = SwipeRefreshLayout.OnRefreshListener{
|
||||
//populate list when pulling to refresh
|
||||
executeTask()
|
||||
}
|
||||
|
||||
fun executeTask(){
|
||||
//clear list before populating
|
||||
list_view.adapter = null
|
||||
|
||||
//create url from url string urlstring
|
||||
val url = Request.Builder().url(urlString).build()
|
||||
//create a okhttpclient for a get request from url
|
||||
val client = OkHttpClient()
|
||||
|
||||
//call the url and retrieve its callback
|
||||
client.newCall(url).enqueue(object: Callback {
|
||||
//failure of retrieval callback
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
//print error to log
|
||||
System.err.println(e)
|
||||
//if swipe refresh is refreshing then stop
|
||||
swipe_refresh.isRefreshing = false
|
||||
//list is empty
|
||||
}
|
||||
|
||||
//successful retrieval callback
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
//get the JSON from the body
|
||||
val urlResponse = response.body?.string()
|
||||
|
||||
//print the response to the logs
|
||||
println("response = $urlResponse")
|
||||
|
||||
//create gson object
|
||||
val gson = Gson()
|
||||
//create a mutable list of objects extracted from the json text
|
||||
val objectList = gson.fromJson(urlResponse, Array<JsonObject>::class.java).asList().toMutableList()
|
||||
|
||||
if (objectList.size >0){
|
||||
//update the ui
|
||||
runOnUiThread{
|
||||
//custom list view adapter created
|
||||
val adapterLV = ListViewAdapter(baseContext, objectList)
|
||||
//apply adapter to listview
|
||||
list_view.adapter = adapterLV
|
||||
//if swipe refresh is refreshing then stop
|
||||
swipe_refresh.isRefreshing = false
|
||||
|
||||
//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
|
||||
}
|
||||
})
|
||||
}
|
||||
}else{
|
||||
//list is empty
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,15 +2,9 @@ package com.example.h_mal.myapplication.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class JsonObject(
|
||||
@SerializedName("name")
|
||||
data class Repo(
|
||||
var name: String? = null,
|
||||
@SerializedName("description")
|
||||
var description : String? = null,
|
||||
@SerializedName("language")
|
||||
var language : String? = null,
|
||||
@SerializedName("created_at")
|
||||
var date : String? = null,
|
||||
@SerializedName("html_url")
|
||||
var repoUrlString : String? = null
|
||||
)
|
||||
var html_url : String? = null)
|
||||
@@ -1,21 +1,20 @@
|
||||
package com.example.h_mal.myapplication
|
||||
package com.example.h_mal.myapplication.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.net.Uri
|
||||
import android.net.UrlQuerySanitizer
|
||||
import android.opengl.Visibility
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import com.example.h_mal.myapplication.model.JsonObject
|
||||
import com.example.h_mal.myapplication.R
|
||||
import com.example.h_mal.myapplication.model.Repo
|
||||
import kotlinx.android.synthetic.main.repo_list_item.view.*
|
||||
|
||||
//custom list adapter extends from array adater
|
||||
class ListViewAdapter(context: Context, objects: MutableList<JsonObject>) :
|
||||
ArrayAdapter<JsonObject>(context, 0, objects){
|
||||
class ListViewAdapter(context: Context, objects: MutableList<Repo>) :
|
||||
ArrayAdapter<Repo>(context, 0, objects){
|
||||
|
||||
//populate each view
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
@@ -36,8 +35,15 @@ class ListViewAdapter(context: Context, objects: MutableList<JsonObject>) :
|
||||
val dateString = item?.date?.split('T')?.get(0)
|
||||
view.date?.text = dateString
|
||||
|
||||
//control the language section of the view
|
||||
populateLanguage(item, view)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
fun populateLanguage(item : Repo?, view : View){
|
||||
//check if the language object is null
|
||||
if (item.language != null){
|
||||
if (item?.language != null){
|
||||
//language exists in object
|
||||
//view holdin the language to be visible
|
||||
view.lang_layout.visibility = View.VISIBLE
|
||||
@@ -49,25 +55,6 @@ class ListViewAdapter(context: Context, objects: MutableList<JsonObject>) :
|
||||
//language was null therefore view to be hidden
|
||||
view.lang_layout.visibility = View.GONE
|
||||
}
|
||||
|
||||
//apply on click listener to item
|
||||
view.setOnClickListener{
|
||||
//click item opens then url
|
||||
openLink(item.repoUrlString)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
//function for opening the link
|
||||
fun openLink(urlString : String?){
|
||||
//open link to repo if the url is not nu;;
|
||||
if (urlString != null){
|
||||
val openURL = Intent(Intent.ACTION_VIEW)
|
||||
openURL.data = Uri.parse(urlString)
|
||||
context.startActivity(openURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//get the corresponding colour based on the programming language
|
||||
@@ -83,4 +70,19 @@ class ListViewAdapter(context: Context, objects: MutableList<JsonObject>) :
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
//function for opening the link
|
||||
fun openLink(position: Int){
|
||||
val urlString = getItem(position)?.html_url
|
||||
//open link to repo if the url is not null
|
||||
if (urlString != null){
|
||||
val openURL = Intent(Intent.ACTION_VIEW)
|
||||
openURL.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
openURL.data = Uri.parse(urlString)
|
||||
context.startActivity(openURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.example.h_mal.myapplication.ui
|
||||
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.support.v4.widget.SwipeRefreshLayout
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.widget.SearchView
|
||||
import com.example.h_mal.myapplication.Api.GetData
|
||||
import com.example.h_mal.myapplication.R
|
||||
import com.example.h_mal.myapplication.model.Repo
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.Retrofit
|
||||
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.observers.DisposableObserver
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
lateinit var searchView: SearchView
|
||||
lateinit var myCompositeDisposable: CompositeDisposable
|
||||
|
||||
val urlString = "https://api.github.com/orgs/square/"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
spinner.visibility = View.VISIBLE
|
||||
|
||||
myCompositeDisposable = CompositeDisposable()
|
||||
|
||||
//begin populating list
|
||||
loadData()
|
||||
|
||||
//set a listener for the swipe to refresh
|
||||
swipe_refresh.setOnRefreshListener(swipeRefreshListener)
|
||||
}
|
||||
|
||||
//implement search interface in the menu
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
//inflate custom menu as our menu
|
||||
menuInflater.inflate(R.menu.menu, menu)
|
||||
//extract searchview
|
||||
val searchItem = menu?.findItem(R.id.search)
|
||||
//set searchview globally
|
||||
searchView = searchItem?.actionView as SearchView
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
myCompositeDisposable?.clear()
|
||||
}
|
||||
|
||||
val swipeRefreshListener = SwipeRefreshLayout.OnRefreshListener{
|
||||
//populate list when pulling to refresh
|
||||
// callData()
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun loadData(){
|
||||
//clear list before populating
|
||||
list_view.adapter = null
|
||||
|
||||
val requestInterface = Retrofit.Builder()
|
||||
.baseUrl(urlString)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.build().create(GetData::class.java)
|
||||
|
||||
myCompositeDisposable.add(requestInterface.getData()
|
||||
.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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
@@ -19,6 +19,41 @@
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"/>
|
||||
<RelativeLayout
|
||||
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
|
||||
android:id="@+id/empty_title_text"
|
||||
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:id="@+id/spinner" app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
@@ -66,12 +66,13 @@ more text more of a description." />
|
||||
|
||||
<LinearLayout android:id="@+id/lang_layout" android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp" android:gravity="right">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="match_parent"
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
tools:text="Java" android:gravity="center" android:id="@+id/lang"/>
|
||||
<android.support.v7.widget.CardView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_gravity="center"
|
||||
app:cardCornerRadius="8dp" app:cardBackgroundColor="#E03F3F" android:id="@+id/lang_col"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user