diff --git a/app/build.gradle b/app/build.gradle index 596df42..6ba6083 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,8 +42,9 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - //mockito + //mockito and livedata testing testImplementation 'org.mockito:mockito-inline:2.13.0' + implementation 'android.arch.core:core-testing' //Retrofit and GSON implementation 'com.squareup.retrofit2:retrofit:2.6.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ea104a..43abf06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> - + diff --git a/app/src/main/java/com/example/h_mal/candyspace/data/api/ApiClass.kt b/app/src/main/java/com/example/h_mal/candyspace/data/api/ApiClass.kt index 61eb50d..23baf68 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/data/api/ApiClass.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/data/api/ApiClass.kt @@ -1,8 +1,7 @@ package com.example.h_mal.candyspace.data.api -import okhttp3.HttpUrl +import com.example.h_mal.candyspace.data.api.model.ApiResponse import okhttp3.OkHttpClient -import okhttp3.Request import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -15,17 +14,21 @@ interface ApiClass { @GET("users?") suspend fun getUsersFromApi(@Query("inname") inname: String): Response + //invoke method creating an invocation of the api call companion object{ operator fun invoke( + //injected @params networkConnectionInterceptor: NetworkConnectionInterceptor, queryParamsInterceptor: QueryParamsInterceptor ) : ApiClass{ + //okHttpClient with interceptors val okkHttpclient = OkHttpClient.Builder() .addNetworkInterceptor(networkConnectionInterceptor) .addInterceptor(queryParamsInterceptor) .build() + //retrofit to be used in @Repository return Retrofit.Builder() .client(okkHttpclient) .baseUrl("https://api.stackexchange.com/2.2/") diff --git a/app/src/main/java/com/example/h_mal/candyspace/data/api/ApiResponse.kt b/app/src/main/java/com/example/h_mal/candyspace/data/api/model/ApiResponse.kt similarity index 55% rename from app/src/main/java/com/example/h_mal/candyspace/data/api/ApiResponse.kt rename to app/src/main/java/com/example/h_mal/candyspace/data/api/model/ApiResponse.kt index ec5e4bb..98aebfe 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/data/api/ApiResponse.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/data/api/model/ApiResponse.kt @@ -1,4 +1,6 @@ -package com.example.h_mal.candyspace.data.api +package com.example.h_mal.candyspace.data.api.model + +import com.example.h_mal.candyspace.data.api.model.User data class ApiResponse( val items : List?, diff --git a/app/src/main/java/com/example/h_mal/candyspace/data/api/BadgeCounts.kt b/app/src/main/java/com/example/h_mal/candyspace/data/api/model/BadgeCounts.kt similarity index 85% rename from app/src/main/java/com/example/h_mal/candyspace/data/api/BadgeCounts.kt rename to app/src/main/java/com/example/h_mal/candyspace/data/api/model/BadgeCounts.kt index 296285b..819c66f 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/data/api/BadgeCounts.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/data/api/model/BadgeCounts.kt @@ -1,4 +1,4 @@ -package com.example.h_mal.candyspace.data.api +package com.example.h_mal.candyspace.data.api.model import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/example/h_mal/candyspace/data/api/User.kt b/app/src/main/java/com/example/h_mal/candyspace/data/api/model/User.kt similarity index 94% rename from app/src/main/java/com/example/h_mal/candyspace/data/api/User.kt rename to app/src/main/java/com/example/h_mal/candyspace/data/api/model/User.kt index 20fa6d4..4e0d89e 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/data/api/User.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/data/api/model/User.kt @@ -1,4 +1,4 @@ -package com.example.h_mal.candyspace.data.api +package com.example.h_mal.candyspace.data.api.model import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/example/h_mal/candyspace/data/repositories/Repository.kt b/app/src/main/java/com/example/h_mal/candyspace/data/repositories/Repository.kt index 7b3e7b0..89a73f9 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/data/repositories/Repository.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/data/repositories/Repository.kt @@ -1,15 +1,15 @@ 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.ApiResponse +import com.example.h_mal.candyspace.data.api.model.ApiResponse import com.example.h_mal.candyspace.data.api.ResponseUnwrap -import com.example.h_mal.candyspace.data.api.User class Repository( private val api: ApiClass ): ResponseUnwrap() { + //get api response from retrofit class + //then unwrap data object from retrofit response class suspend fun getUsers(username: String): ApiResponse { return responseUnwrap { api.getUsersFromApi(username) } } diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainFragment.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/home/MainFragment.kt similarity index 68% rename from app/src/main/java/com/example/h_mal/candyspace/ui/main/MainFragment.kt rename to app/src/main/java/com/example/h_mal/candyspace/ui/home/MainFragment.kt index e33febb..179f375 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainFragment.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/home/MainFragment.kt @@ -1,26 +1,26 @@ -package com.example.h_mal.candyspace.ui.main +package com.example.h_mal.candyspace.ui.home import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.example.h_mal.candyspace.R -import com.example.h_mal.candyspace.data.api.User +import com.example.h_mal.candyspace.data.api.model.User import com.example.h_mal.candyspace.databinding.MainFragmentBinding -import com.example.h_mal.candyspace.ui.MainActivity.Companion.viewModel -import com.example.h_mal.candyspace.utils.displayToast +import com.example.h_mal.candyspace.ui.main.MainActivity.Companion.viewModel +import com.example.h_mal.candyspace.ui.user.ListItemViewModel +import com.example.h_mal.candyspace.ui.user.UserProfileFragment import com.xwray.groupie.GroupAdapter 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(){ companion object { @@ -41,26 +41,37 @@ class MainFragment : Fragment(){ override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - + //observer the live data of what is retrieved from the API call viewModel.usersLiveData.observe(viewLifecycleOwner, Observer { + //create adapter for viewbinding into the recycler view val mAdapter = GroupAdapter().apply { addAll(it.toUserViewModels()) } + //setup the recyclerview binding.recyclerView.apply { layoutManager = LinearLayoutManager(context) setHasFixedSize(true) adapter = mAdapter } + /* + * Item click listener for recyclerView + * + */ mAdapter.setOnItemClickListener { item, _ -> + //get the position of the item clicked val i = mAdapter.getAdapterPosition(item) - - + //set user in @MainViewModel viewModel.setCurrentUser(it[i]) + /** + * display [UserProfileFragment] + */ activity!!.supportFragmentManager.beginTransaction() - .replace(R.id.container, UserProfileFragment()) + .replace(R.id.container, + UserProfileFragment() + ) .addToBackStack("user") .commit() } diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/main/CompletionListener.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/main/CompletionListener.kt index 5a494ec..669ce75 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/main/CompletionListener.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/main/CompletionListener.kt @@ -1,5 +1,8 @@ package com.example.h_mal.candyspace.ui.main +/** + * completion listener for [MainViewModel] when handling async calls + */ interface CompletionListener { fun onStarted() fun onSuccess() diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/MainActivity.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainActivity.kt similarity index 75% rename from app/src/main/java/com/example/h_mal/candyspace/ui/MainActivity.kt rename to app/src/main/java/com/example/h_mal/candyspace/ui/main/MainActivity.kt index 1b74554..6b22aca 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/MainActivity.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainActivity.kt @@ -1,24 +1,22 @@ -package com.example.h_mal.candyspace.ui +package com.example.h_mal.candyspace.ui.main import androidx.appcompat.app.AppCompatActivity 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.ui.main.CompletionListener -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.ui.home.MainFragment 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 +/** + * [MainActivity] hosting the fragments and controlling a lot of the UI + */ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { //retrieve the viewmodel factory from the kodein dependency injection @@ -35,6 +33,7 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) + //setup home button for back navigation supportActionBar?.setDisplayHomeAsUpEnabled(true); supportActionBar?.setDisplayShowHomeEnabled(true); @@ -43,11 +42,15 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { viewModel.completionListener = this if (savedInstanceState == null) { + //display first fragment supportFragmentManager.beginTransaction() - .replace(R.id.container, MainFragment()) + .replace(R.id.container, + MainFragment() + ) .commit() } + //fragment change listener to display title based on current fragment supportFragmentManager.addOnBackStackChangedListener { val name = when (supportFragmentManager.fragments[0]::class.java.simpleName) { "UserProfileFragment" -> "User" @@ -57,6 +60,11 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { } } + /* + * Back button over override + * - close app if home fragment + * - return to previous fragment if User fragment + */ override fun onBackPressed() { if(supportFragmentManager.backStackEntryCount > 0){ supportFragmentManager.popBackStack() @@ -66,6 +74,7 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { } + //When home button in toolbar is pressed override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> onBackPressed() @@ -73,15 +82,21 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { return super.onOptionsItemSelected(item) } + /* + * completion listener methods + */ override fun onStarted() { + //show loading progress_circular.show() } override fun onSuccess() { + //loading completed - hide loading progress_circular.hide() } override fun onFailure(message: String) { + //loading failed - hide loading and display toast of error progress_circular.hide() displayToast(message) } diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModel.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModel.kt index 89410da..49d3d95 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModel.kt @@ -3,15 +3,10 @@ package com.example.h_mal.candyspace.ui.main import android.content.Context 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 com.example.h_mal.candyspace.R -import com.example.h_mal.candyspace.data.api.User +import com.example.h_mal.candyspace.data.api.model.User import com.example.h_mal.candyspace.data.repositories.Repository -import com.squareup.picasso.Picasso import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -21,29 +16,39 @@ class MainViewModel( private val repository: Repository ) : ViewModel() { + // string binded to the edittext input in @R.layout.main_fragment var searchString: String? = null var completionListener: CompletionListener? = null + //data objects - live data and var val usersLiveData = MutableLiveData>() var currentUserLiveData: User? = null - fun submit(view: View){ + /** + * view binding of the submit button in @R.layout.main_fragment + */ + fun submit(view: View?){ + //close keyboard when clicked view.let { v -> - val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.hideSoftInputFromWindow(v.windowToken, 0) + val imm = view?.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.hideSoftInputFromWindow(v?.windowToken, 0) } completionListener?.onStarted() + //return if search string is empty if (searchString.isNullOrEmpty()){ completionListener?.onFailure("Search box is empty") return } + //open a coroutine on the Main thread and update views upon load CoroutineScope(Dispatchers.Main).launch { try { + //retrieve response from API call val apiResponse = repository.getUsers(searchString!!) + //unwrap list of user out of ApiResponse apiResponse.items?.let { if (it.isNotEmpty()){ //update live data diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModelFactory.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModelFactory.kt index b34628f..b646503 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModelFactory.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/main/MainViewModelFactory.kt @@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.example.h_mal.candyspace.data.repositories.Repository +/** + * Viewmodel factory for [MainViewModel] + * @repository injected into MainViewModel + */ @Suppress("UNCHECKED_CAST") class MainViewModelFactory( private val repository: Repository diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/main/ListItemViewModel.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/user/ListItemViewModel.kt similarity index 80% rename from app/src/main/java/com/example/h_mal/candyspace/ui/main/ListItemViewModel.kt rename to app/src/main/java/com/example/h_mal/candyspace/ui/user/ListItemViewModel.kt index 3128681..51bb8d2 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/main/ListItemViewModel.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/user/ListItemViewModel.kt @@ -1,7 +1,7 @@ -package com.example.h_mal.candyspace.ui.main +package com.example.h_mal.candyspace.ui.user import com.example.h_mal.candyspace.R -import com.example.h_mal.candyspace.data.api.User +import com.example.h_mal.candyspace.data.api.model.User import com.example.h_mal.candyspace.databinding.ListItemLayoutBinding import com.xwray.groupie.databinding.BindableItem diff --git a/app/src/main/java/com/example/h_mal/candyspace/ui/main/UserProfileFragment.kt b/app/src/main/java/com/example/h_mal/candyspace/ui/user/UserProfileFragment.kt similarity index 75% rename from app/src/main/java/com/example/h_mal/candyspace/ui/main/UserProfileFragment.kt rename to app/src/main/java/com/example/h_mal/candyspace/ui/user/UserProfileFragment.kt index 452aa3c..9e7cbbe 100644 --- a/app/src/main/java/com/example/h_mal/candyspace/ui/main/UserProfileFragment.kt +++ b/app/src/main/java/com/example/h_mal/candyspace/ui/user/UserProfileFragment.kt @@ -1,15 +1,13 @@ -package com.example.h_mal.candyspace.ui.main +package com.example.h_mal.candyspace.ui.user 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.example.h_mal.candyspace.ui.main.MainActivity.Companion.viewModel import com.squareup.picasso.Picasso /** @@ -20,7 +18,8 @@ import com.squareup.picasso.Picasso class UserProfileFragment : Fragment() { companion object { - fun newInstance() = UserProfileFragment() + fun newInstance() = + UserProfileFragment() } lateinit var binding: FragmentUserProfileBinding @@ -29,13 +28,13 @@ class UserProfileFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - activity?.actionBar?.setHomeButtonEnabled(true) - // Inflate the layout for this fragment + // Inflate the layout for this fragment into data binding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_profile, container, false) return binding.root } + //Update the data for viewbinding onResume as data would have changed when selecting a new user override fun onResume() { super.onResume() viewModel.currentUserLiveData?.let { diff --git a/app/src/main/res/layout/fragment_user_profile.xml b/app/src/main/res/layout/fragment_user_profile.xml index cecf104..251c311 100644 --- a/app/src/main/res/layout/fragment_user_profile.xml +++ b/app/src/main/res/layout/fragment_user_profile.xml @@ -2,14 +2,14 @@ + tools:context=".ui.user.UserProfileFragment"> + type="com.example.h_mal.candyspace.data.api.model.User" /> + type="com.example.h_mal.candyspace.data.api.model.User" /> + tools:context=".ui.main.MainActivity" > + tools:context=".ui.home.MainFragment">