View binding completed for both fragments and item view layout.

Data populated in recycler view completed.
Back button navigation completed.
Title for fragments completed.
Displaying of profile images with Picasso added.
This commit is contained in:
2020-04-20 16:30:02 +01:00
parent a5dffc276c
commit 5380cd0c74
23 changed files with 531 additions and 40 deletions

1
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

View File

@@ -38,6 +38,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@@ -69,4 +70,5 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.1.0" implementation "androidx.preference:preference-ktx:1.1.0"
implementation 'com.squareup.picasso:picasso:2.71828'
} }

View File

@@ -3,7 +3,10 @@
package="com.example.h_mal.candyspace"> package="com.example.h_mal.candyspace">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<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"

View File

@@ -1,14 +1,30 @@
package com.example.h_mal.candyspace package com.example.h_mal.candyspace
import android.app.Application import android.app.Application
import com.example.h_mal.candyspace.data.api.ApiClass
import com.example.h_mal.candyspace.data.api.NetworkConnectionInterceptor
import com.example.h_mal.candyspace.data.api.QueryParamsInterceptor
import com.example.h_mal.candyspace.data.repositories.Repository
import com.example.h_mal.candyspace.ui.main.MainViewModelFactory
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule 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{ class AppClass : Application(), KodeinAware{
override val kodein = Kodein.lazy { override val kodein = Kodein.lazy {
import(androidXModule(this@AppClass)) import(androidXModule(this@AppClass))
bind() from singleton { NetworkConnectionInterceptor(instance()) }
bind() from singleton { QueryParamsInterceptor() }
bind() from singleton { ApiClass(instance(), instance()) }
// bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { Repository(instance()) }
bind() from provider { MainViewModelFactory(instance()) }
} }
} }

View File

@@ -13,7 +13,7 @@ import retrofit2.http.Query
interface ApiClass { interface ApiClass {
@GET("users?") @GET("users?")
suspend fun getUsersFromApi(@Query("inname") inname: String): Response<User> suspend fun getUsersFromApi(@Query("inname") inname: String): Response<ApiResponse>
companion object{ companion object{
operator fun invoke( operator fun invoke(

View File

@@ -0,0 +1,8 @@
package com.example.h_mal.candyspace.data.api
data class ApiResponse(
val items : List<User>?,
val has_more : Boolean?,
val quota_max : Int?,
val quota_Remaining: Int?
)

View File

@@ -0,0 +1,16 @@
package com.example.h_mal.candyspace.data.api
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
class BadgeCounts {
@SerializedName("bronze")
@Expose
var bronze: Int? = null
@SerializedName("silver")
@Expose
var silver: Int? = null
@SerializedName("gold")
@Expose
var gold: Int? = null
}

View File

@@ -7,7 +7,7 @@ import java.io.IOException
abstract class ResponseUnwrap { abstract class ResponseUnwrap {
suspend fun<T: Any> apiRequest(call: suspend () -> Response<T>) : T{ suspend fun<T: Any> responseUnwrap(call: suspend () -> Response<T>) : T{
val response = call.invoke() val response = call.invoke()
if(response.isSuccessful){ if(response.isSuccessful){

View File

@@ -5,6 +5,9 @@ import com.google.gson.annotations.SerializedName
class User { class User {
@SerializedName("badge_counts")
@Expose
var badgeCounts: BadgeCounts? = null
@SerializedName("last_modified_date") @SerializedName("last_modified_date")
@Expose @Expose
var lastModifiedDate: Int? = null var lastModifiedDate: Int? = null

View File

@@ -1,6 +1,8 @@
package com.example.h_mal.candyspace.data.repositories package com.example.h_mal.candyspace.data.repositories
import androidx.lifecycle.LiveData
import com.example.h_mal.candyspace.data.api.ApiClass import com.example.h_mal.candyspace.data.api.ApiClass
import com.example.h_mal.candyspace.data.api.ApiResponse
import com.example.h_mal.candyspace.data.api.ResponseUnwrap import com.example.h_mal.candyspace.data.api.ResponseUnwrap
import com.example.h_mal.candyspace.data.api.User import com.example.h_mal.candyspace.data.api.User
@@ -8,7 +10,7 @@ class Repository(
private val api: ApiClass private val api: ApiClass
): ResponseUnwrap() { ): ResponseUnwrap() {
suspend fun getUsers(username: String): User { suspend fun getUsers(username: String): ApiResponse {
return apiRequest { api.getUsersFromApi(username) } return responseUnwrap { api.getUsersFromApi(username) }
} }
} }

View File

@@ -2,18 +2,87 @@ package com.example.h_mal.candyspace.ui
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import com.example.h_mal.candyspace.R import com.example.h_mal.candyspace.R
import com.example.h_mal.candyspace.ui.main.CompletionListener
import com.example.h_mal.candyspace.ui.main.MainFragment import com.example.h_mal.candyspace.ui.main.MainFragment
import com.example.h_mal.candyspace.ui.main.MainViewModel
import com.example.h_mal.candyspace.ui.main.MainViewModelFactory
import com.example.h_mal.candyspace.utils.displayToast
import com.example.h_mal.candyspace.utils.hide
import com.example.h_mal.candyspace.utils.show
import kotlinx.android.synthetic.main.main_activity.*
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.generic.instance
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
//retrieve the viewmodel factory from the kodein dependency injection
override val kodein by kodein()
private val factory : MainViewModelFactory by instance()
companion object{
//View model to be used by the fragments hosted by MainActivity
lateinit var viewModel: MainViewModel
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity) setContentView(R.layout.main_activity)
supportActionBar?.setDisplayHomeAsUpEnabled(true);
supportActionBar?.setDisplayShowHomeEnabled(true);
//retrieve viewmodel from viewmodel factory
viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)
viewModel.completionListener = this
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment.newInstance()) .replace(R.id.container, MainFragment())
.commitNow() .commit()
}
supportFragmentManager.addOnBackStackChangedListener {
val name = when (supportFragmentManager.fragments[0]::class.java.simpleName) {
"UserProfileFragment" -> "User"
else -> "Candy space"
}
title = name
} }
} }
override fun onBackPressed() {
if(supportFragmentManager.backStackEntryCount > 0){
supportFragmentManager.popBackStack()
}else{
super.onBackPressed()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
}
override fun onStarted() {
progress_circular.show()
}
override fun onSuccess() {
progress_circular.hide()
}
override fun onFailure(message: String) {
progress_circular.hide()
displayToast(message)
}
} }

View File

@@ -0,0 +1,16 @@
package com.example.h_mal.candyspace.ui.main
import com.example.h_mal.candyspace.R
import com.example.h_mal.candyspace.data.api.User
import com.example.h_mal.candyspace.databinding.ListItemLayoutBinding
import com.xwray.groupie.databinding.BindableItem
class ListItemViewModel (
val user: User
): BindableItem<ListItemLayoutBinding>(){
override fun getLayout(): Int = R.layout.list_item_layout
override fun bind(viewBinding: ListItemLayoutBinding, position: Int) {
viewBinding.user = user
}
}

View File

@@ -1,42 +1,40 @@
package com.example.h_mal.candyspace.ui.main package com.example.h_mal.candyspace.ui.main
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.h_mal.candyspace.R import com.example.h_mal.candyspace.R
import com.example.h_mal.candyspace.data.api.User
import com.example.h_mal.candyspace.databinding.MainFragmentBinding import com.example.h_mal.candyspace.databinding.MainFragmentBinding
import org.kodein.di.Kodein import com.example.h_mal.candyspace.ui.MainActivity.Companion.viewModel
import org.kodein.di.KodeinAware import com.example.h_mal.candyspace.utils.displayToast
import org.kodein.di.android.kodein import com.xwray.groupie.GroupAdapter
import org.kodein.di.generic.instance import com.xwray.groupie.ViewHolder
/* /*
* UI for the first screen holding the list, search box * UI for the first screen holding the list, search box
*/ */
class MainFragment : Fragment(), KodeinAware{ class MainFragment : Fragment(){
//retrieve the viewmodel factory from the kodein dependency injection
override val kodein by lazy { (context as KodeinAware).kodein }
private val factory : MainViewModelFactory by instance()
companion object { companion object {
fun newInstance() = MainFragment() fun newInstance() = MainFragment()
} }
private lateinit var viewModel: MainViewModel lateinit var binding: MainFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View { savedInstanceState: Bundle?): View {
//retrieve viewmodel from viewmodel factory //Bind layout to Viewmodel in main activity
viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java) binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
//Bind layout to Viewmodel
val binding: MainFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
binding.viewmodel = viewModel binding.viewmodel = viewModel
binding.lifecycleOwner = this
return binding.root return binding.root
} }
@@ -44,8 +42,37 @@ class MainFragment : Fragment(), KodeinAware{
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel.usersLiveData.observe(viewLifecycleOwner, Observer {
val mAdapter = GroupAdapter<ViewHolder>().apply {
addAll(it.toUserViewModels())
} }
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
adapter = mAdapter
}
mAdapter.setOnItemClickListener { item, _ ->
val i = mAdapter.getAdapterPosition(item)
viewModel.setCurrentUser(it[i])
activity!!.supportFragmentManager.beginTransaction()
.replace(R.id.container, UserProfileFragment())
.addToBackStack("user")
.commit()
}
})
}
private fun List<User>.toUserViewModels() : List<ListItemViewModel>{
return this.map {
ListItemViewModel(it)
}
}
} }

View File

@@ -1,8 +1,17 @@
package com.example.h_mal.candyspace.ui.main package com.example.h_mal.candyspace.ui.main
import android.content.Context
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import androidx.core.content.ContextCompat.getSystemService
import androidx.databinding.BindingAdapter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.example.h_mal.candyspace.R
import com.example.h_mal.candyspace.data.api.User
import com.example.h_mal.candyspace.data.repositories.Repository import com.example.h_mal.candyspace.data.repositories.Repository
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -14,9 +23,17 @@ class MainViewModel(
var searchString: String? = null var searchString: String? = null
val completionListener: CompletionListener? = null var completionListener: CompletionListener? = null
val usersLiveData = MutableLiveData<List<User>>()
var currentUserLiveData: User? = null
fun submit(view: View){ fun submit(view: View){
view.let { v ->
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(v.windowToken, 0)
}
completionListener?.onStarted() completionListener?.onStarted()
if (searchString.isNullOrEmpty()){ if (searchString.isNullOrEmpty()){
completionListener?.onFailure("Search box is empty") completionListener?.onFailure("Search box is empty")
@@ -25,15 +42,25 @@ class MainViewModel(
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
val user = repository.getUsers(searchString!!) val apiResponse = repository.getUsers(searchString!!)
//todo: update live data
apiResponse.items?.let {
if (it.isNotEmpty()){
//update live data
usersLiveData.value = it
//successfully finished //successfully finished
completionListener?.onSuccess() completionListener?.onSuccess()
return@launch
}
}
completionListener?.onFailure("No Users found")
}catch (e: IOException){ }catch (e: IOException){
completionListener?.onFailure(e.message!!) completionListener?.onFailure(e.message!!)
} }
completionListener?.onFailure("Failed to retrieve Users")
} }
} }
fun setCurrentUser(user: User) {
currentUserLiveData = user
}
} }

View File

@@ -0,0 +1,49 @@
package com.example.h_mal.candyspace.ui.main
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import com.example.h_mal.candyspace.R
import com.example.h_mal.candyspace.databinding.FragmentUserProfileBinding
import com.example.h_mal.candyspace.ui.MainActivity
import com.example.h_mal.candyspace.ui.MainActivity.Companion.viewModel
import com.squareup.picasso.Picasso
/**
* A simple [Fragment] subclass.
* Use the [UserProfileFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class UserProfileFragment : Fragment() {
companion object {
fun newInstance() = UserProfileFragment()
}
lateinit var binding: FragmentUserProfileBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity?.actionBar?.setHomeButtonEnabled(true)
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_profile, container, false)
return binding.root
}
override fun onResume() {
super.onResume()
viewModel.currentUserLiveData?.let {
binding.user = it
binding.lifecycleOwner = this
Picasso.get().load(it.profileImage).placeholder(R.mipmap.ic_launcher).into(binding.imageView)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.example.h_mal.candyspace.utils
import com.squareup.picasso.Picasso
import java.text.SimpleDateFormat
import java.util.*
object ConverterUtil {
@JvmStatic fun epochToData(number: Int): String {
return try {
val sdf = SimpleDateFormat("dd/MM/yyyy")
val netDate = Date(number.toLong() * 1000)
sdf.format(netDate)
} catch (e: Exception) {
"Unspecified"
}
}
}

View File

@@ -0,0 +1,17 @@
package com.example.h_mal.candyspace.utils
import android.content.Context
import android.view.View
import android.widget.Toast
fun View.show(){
this.visibility = View.VISIBLE
}
fun View.hide(){
this.visibility = View.GONE
}
fun Context.displayToast(message: String){
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".ui.main.UserProfileFragment">
<data>
<import type="com.example.h_mal.candyspace.R"/>
<import type="com.example.h_mal.candyspace.utils.ConverterUtil"/>
<variable
name="user"
type="com.example.h_mal.candyspace.data.api.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="108dp"
android:layout_height="108dp"
android:layout_marginTop="48dp"
android:src="@mipmap/ic_launcher"
tools:src="@drawable/ic_launcher_foreground"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/username_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/imageView"
app:layout_constraintRight_toLeftOf="@id/divider"
android:layout_marginTop="12dp"
android:text="Username:" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
tools:text="Username1234"
android:text="@{user.displayName}"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<TextView
android:id="@+id/reputation_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/username_label"
app:layout_constraintRight_toLeftOf="@id/divider"
android:layout_marginTop="12dp"
android:text="Reputation:" />
<TextView
android:id="@+id/reputation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
tools:text="1548252"
android:text="@{user.reputation.toString()}"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView
android:id="@+id/badges_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/reputation_label"
app:layout_constraintRight_toLeftOf="@id/divider"
android:layout_marginTop="12dp"
android:text="Bagdes:" />
<TextView
android:id="@+id/badges_gold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Gold - "
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/reputation" />
<TextView
android:id="@+id/gold_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3554"
android:text="@{user.badgeCounts.gold.toString()}"
app:layout_constraintLeft_toRightOf="@id/badges_gold"
app:layout_constraintTop_toTopOf="@id/badges_gold"/>
<TextView
android:id="@+id/badges_silver"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="Silver - "
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/badges_gold" />
<TextView
android:id="@+id/silver_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3554"
android:text="@{user.badgeCounts.silver.toString()}"
app:layout_constraintLeft_toRightOf="@id/badges_silver"
app:layout_constraintTop_toTopOf="@id/badges_silver"/>
<TextView
android:id="@+id/badges_bronze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="Bronze - "
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/badges_silver" />
<TextView
android:id="@+id/bronze_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3554"
android:text="@{user.badgeCounts.bronze.toString()}"
app:layout_constraintLeft_toRightOf="@id/badges_bronze"
app:layout_constraintTop_toTopOf="@id/badges_bronze"/>
<TextView
android:id="@+id/date_joined_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/badges_bronze"
app:layout_constraintRight_toLeftOf="@id/divider"
android:layout_marginTop="12dp"
android:text="Date Joined:" />
<TextView
android:id="@+id/date_joined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
tools:text="31/01/2019"
android:text="@{ConverterUtil.epochToData(user.creationDate)}"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/badges_bronze" />
<Space
android:id="@+id/divider"
android:layout_width="24dp"
android:layout_height="0dp"
android:layout_marginBottom="12dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.example.h_mal.candyspace.data.api.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="12dp"
tools:text="12345"
android:text="@{user.userId.toString()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
tools:text="Username"
android:text="@{user.displayName}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/textView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -4,4 +4,13 @@
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.MainActivity" /> tools:context=".ui.MainActivity" >
<ProgressBar
android:id="@+id/progress_circular"
android:visibility="gone"
tools:visibility="visible"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>

View File

@@ -23,11 +23,16 @@
android:layout_margin="4dp" android:layout_margin="4dp"
android:orientation="horizontal" android:orientation="horizontal"
android:id="@+id/container"> android:id="@+id/container">
<EditText <EditText
android:id="@+id/search_bar" android:id="@+id/search_bar"
android:layout_weight="1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="text"
android:imeOptions="actionDone"
android:maxLines="1"
android:text="@={viewmodel.searchString}" />
<Button <Button
android:id="@+id/submit" android:id="@+id/submit"
@@ -36,6 +41,7 @@
app:layout_constraintStart_toEndOf="@id/search_bar" app:layout_constraintStart_toEndOf="@id/search_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:onClick="@{viewmodel::submit}"
android:text="Submit"/> android:text="Submit"/>
</LinearLayout> </LinearLayout>

View File

@@ -1,3 +1,6 @@
<resources> <resources>
<string name="app_name">Candy Space</string> <string name="app_name">Candy Space</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>

View File

@@ -1,9 +1,8 @@
package com.example.h_mal.candyspace.data.repositories package com.example.h_mal.candyspace.data.repositories
import android.util.Log
import com.example.h_mal.candyspace.data.api.ApiClass import com.example.h_mal.candyspace.data.api.ApiClass
import com.example.h_mal.candyspace.data.api.ApiResponse
import com.example.h_mal.candyspace.data.api.User import com.example.h_mal.candyspace.data.api.User
import com.google.gson.Gson
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.ResponseBody import okhttp3.ResponseBody
@@ -11,7 +10,6 @@ import org.junit.Before
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
@@ -33,8 +31,8 @@ class RepositoryTest {
@Test @Test
fun fetchUserFromApi_positiveResponse() = runBlocking { fun fetchUserFromApi_positiveResponse() = runBlocking {
val mockUser = mock(User::class.java) val mockApiResponse = mock(ApiResponse::class.java)
val mockResponse = Response.success(mockUser) val mockResponse = Response.success(mockApiResponse)
Mockito.`when`(api.getUsersFromApi("12345")).thenReturn( Mockito.`when`(api.getUsersFromApi("12345")).thenReturn(
mockResponse mockResponse
@@ -43,12 +41,12 @@ class RepositoryTest {
val getUser = repository.getUsers("12345") val getUser = repository.getUsers("12345")
assertNotNull(getUser) assertNotNull(getUser)
assertEquals(mockUser, getUser) assertEquals(mockApiResponse, getUser)
} }
@Test @Test
fun fetchUserFromApi_negativeResponse() = runBlocking { fun fetchUserFromApi_negativeResponse() = runBlocking {
val mockResponse = Response.error<User>(403, val mockResponse = Response.error<ApiResponse>(403,
ResponseBody.create( ResponseBody.create(
MediaType.parse("application/json"), MediaType.parse("application/json"),
"{\"key\":[\"somestuff\"]}" "{\"key\":[\"somestuff\"]}"