From a99a73b718ad56fae8d51879b3075bdc261d1ef9 Mon Sep 17 00:00:00 2001 From: Krunal Patel Date: Fri, 9 Jun 2023 18:40:58 +0530 Subject: [PATCH] TE7-T893: Add OkHttp and GSON - Parse data from and to JSON - Make GET, POST, PUT, DELETE request --- Demo/app/build.gradle | 6 + .../demo/appcomponents/utils/IntentData.kt | 1 + .../repositories/ComponentDetailRepository.kt | 6 + .../ui/activities/MainActivity.kt | 7 + .../com/krunal/demo/utils/AppConstants.kt | 15 ++- .../gsonokhttp/data/apis/UserProfileApi.kt | 110 +++++++++++++++ .../gsonokhttp/data/models/api/Movie.kt | 5 + .../gsonokhttp/data/models/api/UserDetail.kt | 15 +++ .../data/models/local/UserRegistration.kt | 47 +++++++ .../gsonokhttp/repositories/UserRepository.kt | 29 ++++ .../gsonokhttp/ui/adapters/UserAdapter.kt | 45 +++++++ .../ui/dialogs/DeleteConfirmationDialog.kt | 21 +++ .../ui/fragments/UserDetailFragment.kt | 57 ++++++++ .../ui/fragments/UserListFragment.kt | 70 ++++++++++ .../ui/viewmodels/UserRegisterViewModel.kt | 127 ++++++++++++++++++ .../gsonokhttp/ui/viewmodels/UserViewModel.kt | 47 +++++++ .../krunal/demo/webservices/utils/Resource.kt | 8 ++ .../withoutlibrary/data/apis/NewsApi.kt | 2 +- .../app/src/main/res/layout/activity_main.xml | 1 - .../main/res/layout/fragment_user_detail.xml | 102 ++++++++++++++ .../main/res/layout/fragment_user_list.xml | 45 +++++++ .../res/layout/fragment_user_register.xml | 123 +++++++++++++++++ .../src/main/res/layout/item_user_detail.xml | 56 ++++++++ .../main/res/menu/search_web_bottom_nav.xml | 2 +- .../main/res/menu/trivia_game_bottom_nav.xml | 2 +- Demo/app/src/main/res/values/dimens.xml | 2 + Demo/app/src/main/res/values/strings.xml | 11 +- 27 files changed, 956 insertions(+), 6 deletions(-) create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/apis/UserProfileApi.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/Movie.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/UserDetail.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/local/UserRegistration.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/repositories/UserRepository.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/adapters/UserAdapter.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/dialogs/DeleteConfirmationDialog.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserDetailFragment.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserListFragment.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserRegisterViewModel.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserViewModel.kt create mode 100644 Demo/app/src/main/java/com/krunal/demo/webservices/utils/Resource.kt create mode 100644 Demo/app/src/main/res/layout/fragment_user_detail.xml create mode 100644 Demo/app/src/main/res/layout/fragment_user_list.xml create mode 100644 Demo/app/src/main/res/layout/fragment_user_register.xml create mode 100644 Demo/app/src/main/res/layout/item_user_detail.xml diff --git a/Demo/app/build.gradle b/Demo/app/build.gradle index 39c8592..c88fc9a 100644 --- a/Demo/app/build.gradle +++ b/Demo/app/build.gradle @@ -59,6 +59,12 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + // Gson + implementation "com.google.code.gson:gson:2.10.1" + + // OkHttp3 + implementation "com.squareup.okhttp3:okhttp:4.10.0" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt b/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt index f62f43a..0c1b2d6 100644 --- a/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt +++ b/Demo/app/src/main/java/com/krunal/demo/appcomponents/utils/IntentData.kt @@ -8,6 +8,7 @@ object IntentData { const val MESSAGE = "message" const val AUTH_KEY = "auth_key" const val WEB_URL = "web_url" + const val USER_ID = "user_id" /** * Actions diff --git a/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt b/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt index 5659dc3..9ed8b68 100644 --- a/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt +++ b/Demo/app/src/main/java/com/krunal/demo/mainnavigation/data/repositories/ComponentDetailRepository.kt @@ -34,6 +34,8 @@ import com.krunal.demo.uicomponents.constraintLayouts.GuidelineBarrierFragment import com.krunal.demo.uicomponents.constraintLayouts.RelativeFragment import com.krunal.demo.uicomponents.picker.DatePickerFragment import com.krunal.demo.uicomponents.picker.TimePickerFragment +import com.krunal.demo.webservices.gsonokhttp.ui.fragments.UserListFragment +import com.krunal.demo.webservices.gsonokhttp.ui.fragments.UserDetailFragment import com.krunal.demo.webservices.withoutlibrary.ui.fragments.NewsListFragment object ComponentDetailRepository { @@ -58,6 +60,10 @@ object ComponentDetailRepository { "Search and Web view", SearchWebActivity::class.java ), ComponentDetail.FragmentComponent( "News List", NewsListFragment::class.java + ), ComponentDetail.FragmentComponent( + "User List", UserListFragment::class.java + ), ComponentDetail.FragmentComponent( + "Register User", UserDetailFragment::class.java ) ) } diff --git a/Demo/app/src/main/java/com/krunal/demo/mainnavigation/ui/activities/MainActivity.kt b/Demo/app/src/main/java/com/krunal/demo/mainnavigation/ui/activities/MainActivity.kt index 5bc5109..b43aafa 100644 --- a/Demo/app/src/main/java/com/krunal/demo/mainnavigation/ui/activities/MainActivity.kt +++ b/Demo/app/src/main/java/com/krunal/demo/mainnavigation/ui/activities/MainActivity.kt @@ -28,6 +28,13 @@ class MainActivity : AppCompatActivity() { installSplashScreen() setupTheme() setContentView(binding.root) + setupHostFragment() + } + + private fun setupHostFragment() { + supportFragmentManager.commit { + replace(R.id.hostFragmentContainer, HostFragment::class.java, null) + } } private fun setupTheme() { diff --git a/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt b/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt index 3b37918..c9e4139 100644 --- a/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt +++ b/Demo/app/src/main/java/com/krunal/demo/utils/AppConstants.kt @@ -2,6 +2,19 @@ package com.krunal.demo.utils object AppConstants { + /** + * Log Tag + */ + const val TAG = "Demo" + + /** + * News + */ const val NEWS_API_KEY = "fa3d5d7db3f8448dac9f98e82f1108ce" - const val NEST_BASE_URL = "https://newsapi.org/v2" + const val NEWS_BASE_URL = "https://newsapi.org/v2" + + /** + * User + */ + const val MOCKAPI_BASE_URL = "https://6482a62bf2e76ae1b95b5f48.mockapi.io/v1" } \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/apis/UserProfileApi.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/apis/UserProfileApi.kt new file mode 100644 index 0000000..476194b --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/apis/UserProfileApi.kt @@ -0,0 +1,110 @@ +package com.krunal.demo.webservices.gsonokhttp.data.apis + +import com.google.gson.GsonBuilder +import com.google.gson.JsonParseException +import com.krunal.demo.utils.AppConstants +import com.krunal.demo.webservices.gsonokhttp.data.models.api.UserDetail +import com.krunal.demo.webservices.utils.Resource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException + +object UserProfileApi { + + private const val USER_URL = "${AppConstants.MOCKAPI_BASE_URL}/users" + private const val DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ" + + private val client = OkHttpClient() + + suspend fun getAllUsers() = flow>> { + emit(Resource.Loading()) + val request = Request.Builder().url(USER_URL).get().build() + + try { + val response = client.newCall(request).execute() + Resource.Success("") + + if (!response.isSuccessful) { + emit(Resource.Error(response.message)) + } + + response.body?.string()?.let { body -> + val gson = GsonBuilder().setDateFormat(DATE_FORMAT).create() + + val users = gson.fromJson(body, Array::class.java).toList() + emit(Resource.Success(users)) + } + + } catch (e: JsonParseException) { + emit(Resource.Error(e.message ?: "Can't parse response")) + } catch (e: IOException) { + emit(Resource.Error(e.message ?: "Can't make request")) + } + }.flowOn(Dispatchers.IO) + + suspend fun getUser(userId: Int) = flow> { + val request = Request.Builder().url("$USER_URL/$userId").get().build() + + try { + val response = client.newCall(request).execute() + Resource.Success("") + + if (!response.isSuccessful) { + emit(Resource.Error(response.message)) + } + + response.body?.string()?.let { body -> + val gson = GsonBuilder().setDateFormat(DATE_FORMAT).create() + + val user = gson.fromJson(body, UserDetail::class.java) + emit(Resource.Success(user)) + } + + } catch (e: JsonParseException) { + emit(Resource.Error(e.message ?: "Can't parse response")) + } catch (e: IOException) { + emit(Resource.Error(e.message ?: "Can't make request")) + } + }.flowOn(Dispatchers.IO) + + suspend fun registerUser(userDetail: UserDetail) { + withContext(Dispatchers.IO) { + val gson = GsonBuilder().setDateFormat(DATE_FORMAT).create() + val user = gson.toJson(userDetail, UserDetail::class.java) + val body = user.toRequestBody("application/json".toMediaType()) + val request = Request.Builder().url(USER_URL).post(body) + .addHeader("Content-Type", "application/json").build() + + client.newCall(request).execute() + } + } + + suspend fun updateUser(userDetail: UserDetail) { + withContext(Dispatchers.IO) { + val gson = GsonBuilder().setDateFormat(DATE_FORMAT).create() + val user = gson.toJson(userDetail, UserDetail::class.java) + val body = user.toRequestBody("application/json".toMediaType()) + val request = Request.Builder().url("$USER_URL/${userDetail.userId}") + .put(body) + .addHeader("Content-Type", "application/json").build() + + client.newCall(request).execute() + } + } + + suspend fun deleteUser(userId: Int) { + withContext(Dispatchers.IO) { + val request = Request.Builder().url("$USER_URL/${userId}") + .delete() + .build() + + client.newCall(request).execute() + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/Movie.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/Movie.kt new file mode 100644 index 0000000..b252329 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/Movie.kt @@ -0,0 +1,5 @@ +package com.krunal.demo.webservices.gsonokhttp.data.models.api + +data class Movie( + val name: String +) diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/UserDetail.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/UserDetail.kt new file mode 100644 index 0000000..330d76c --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/api/UserDetail.kt @@ -0,0 +1,15 @@ +package com.krunal.demo.webservices.gsonokhttp.data.models.api + +import com.google.gson.annotations.SerializedName +import java.util.Date + +data class UserDetail( + @SerializedName("id") + val userId: Int? = null, + val name: String? = null, + @SerializedName("avatar") + val avatarUrl: String? = null, + val email: String? = null, + val dob: Date? = null, + val favoriteMovies: List = emptyList() +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/local/UserRegistration.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/local/UserRegistration.kt new file mode 100644 index 0000000..1fed924 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/data/models/local/UserRegistration.kt @@ -0,0 +1,47 @@ +package com.krunal.demo.webservices.gsonokhttp.data.models.local + +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import com.krunal.demo.BR +import java.util.Date + +data class UserRegistration( + var userId: Int? = null, + private var _name: String? = null, + private var _email: String? = null, + private var _dob: Date? = null, + private var _movies: String? = null +) : BaseObservable() { + + @get:Bindable + var name: String? = _name + set(value) { + _name = value + field = value + notifyPropertyChanged(BR.name) + } + + @get:Bindable + var email: String? = _email + set(value) { + _email = value + field = value + notifyPropertyChanged(BR.email) + } + + @get:Bindable + var dob: Date? = _dob + set(value) { + _dob = value + field = value + notifyPropertyChanged(BR.dob) + } + + @get:Bindable + var movies: String? = _movies + set(value) { + _movies = value + field = value + notifyPropertyChanged(BR.movies) + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/repositories/UserRepository.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/repositories/UserRepository.kt new file mode 100644 index 0000000..f24d708 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/repositories/UserRepository.kt @@ -0,0 +1,29 @@ +package com.krunal.demo.webservices.gsonokhttp.repositories + +import com.krunal.demo.webservices.gsonokhttp.data.apis.UserProfileApi +import com.krunal.demo.webservices.gsonokhttp.data.models.api.UserDetail +import com.krunal.demo.webservices.utils.Resource +import kotlinx.coroutines.flow.Flow + +object UserRepository { + + suspend fun getAllUsers(): Flow>> { + return UserProfileApi.getAllUsers() + } + + suspend fun getUser(userId: Int): Flow> { + return UserProfileApi.getUser(userId) + } + + suspend fun registerUser(userDetail: UserDetail) { + UserProfileApi.registerUser(userDetail) + } + + suspend fun updateUser(userDetail: UserDetail) { + UserProfileApi.updateUser(userDetail) + } + + suspend fun deleteUser(userId: Int) { + UserProfileApi.deleteUser(userId) + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/adapters/UserAdapter.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/adapters/UserAdapter.kt new file mode 100644 index 0000000..7efd347 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/adapters/UserAdapter.kt @@ -0,0 +1,45 @@ +package com.krunal.demo.webservices.gsonokhttp.ui.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.krunal.demo.databinding.ItemUserDetailBinding +import com.krunal.demo.webservices.gsonokhttp.data.models.api.UserDetail + +class UserAdapter(private val onClick: (userDetail: UserDetail) -> Unit) : RecyclerView.Adapter() { + + private val newsList: MutableList = mutableListOf() + + class UserDetailHolder( + private val binding: ItemUserDetailBinding, + val onClick: (UserDetail) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(userDetail: UserDetail) { + binding.userDetail = userDetail + binding.root.setOnClickListener { + onClick(userDetail) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserDetailHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = ItemUserDetailBinding.inflate(layoutInflater, parent, false) + return UserDetailHolder(binding, onClick) + } + + override fun getItemCount(): Int = newsList.count() + + override fun onBindViewHolder(holder: UserDetailHolder, position: Int) { + holder.bind(newsList[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun submitList(list: List) { + newsList.clear() + newsList.addAll(list) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/dialogs/DeleteConfirmationDialog.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/dialogs/DeleteConfirmationDialog.kt new file mode 100644 index 0000000..001aad0 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/dialogs/DeleteConfirmationDialog.kt @@ -0,0 +1,21 @@ +package com.krunal.demo.webservices.gsonokhttp.ui.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.krunal.demo.R + +class DeleteConfirmationDialog(private val onConfirm: () -> Unit) : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.delete_confirmation)) + .setCancelable(true) + .setPositiveButton(getString(R.string.ok)) { _, _ -> onConfirm() } + .create() + + companion object { + const val TAG = "DeleteConfirmationDialog" + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserDetailFragment.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserDetailFragment.kt new file mode 100644 index 0000000..44a68f1 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserDetailFragment.kt @@ -0,0 +1,57 @@ +package com.krunal.demo.webservices.gsonokhttp.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.krunal.demo.R +import com.krunal.demo.appcomponents.utils.IntentData +import com.krunal.demo.databinding.FragmentUserRegisterBinding +import com.krunal.demo.webservices.gsonokhttp.ui.dialogs.DeleteConfirmationDialog +import com.krunal.demo.webservices.gsonokhttp.ui.viewmodels.UserRegisterViewModel + +class UserDetailFragment : Fragment() { + + private lateinit var binding: FragmentUserRegisterBinding + private val viewModel: UserRegisterViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + binding = FragmentUserRegisterBinding.inflate(layoutInflater, container, false) + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + binding.btnSubmit + binding.executePendingBindings() + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUI() + } + + private fun setupUI() { + arguments?.getInt(IntentData.USER_ID)?.toInt()?.let(viewModel::setUpdateUser) + + binding.btnDeleteAccount.setOnClickListener { + DeleteConfirmationDialog { + deleteUser() + }.show(childFragmentManager, DeleteConfirmationDialog.TAG) + } + } + + private fun deleteUser() { + viewModel.deleteUser { deleted -> + if (deleted) { + Toast.makeText(requireContext(), getString(R.string.user_delete_success, viewModel.userDetails.value.name), Toast.LENGTH_SHORT).show() + parentFragmentManager.popBackStack() + } else { + Toast.makeText(requireContext(), getString(R.string.user_delete_failed, viewModel.userDetails.value.name), Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserListFragment.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserListFragment.kt new file mode 100644 index 0000000..2da3f53 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/fragments/UserListFragment.kt @@ -0,0 +1,70 @@ +package com.krunal.demo.webservices.gsonokhttp.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.fragment.app.commitNow +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import com.krunal.demo.R +import com.krunal.demo.appcomponents.utils.IntentData +import com.krunal.demo.databinding.FragmentUserListBinding +import com.krunal.demo.webservices.gsonokhttp.ui.adapters.UserAdapter +import com.krunal.demo.webservices.gsonokhttp.ui.viewmodels.UserViewModel +import kotlinx.coroutines.launch + +class UserListFragment : Fragment() { + + private lateinit var binding: FragmentUserListBinding + private val viewModel: UserViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + binding = FragmentUserListBinding.inflate(layoutInflater, container, false) + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUI() + } + + private fun setupUI() { + val adapter = UserAdapter { userDetail -> + parentFragmentManager.commit { + replace( + R.id.hostFragmentContainer, + UserDetailFragment::class.java, + bundleOf(IntentData.USER_ID to userDetail.userId) + ) + } + } + + binding.rvUsers.apply { + this.adapter = adapter + addItemDecoration( + DividerItemDecoration( + requireContext(), DividerItemDecoration.VERTICAL + ) + ) + } + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.users.collect { list -> + adapter.submitList(list) + } + } + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserRegisterViewModel.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserRegisterViewModel.kt new file mode 100644 index 0000000..f209b0e --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserRegisterViewModel.kt @@ -0,0 +1,127 @@ +package com.krunal.demo.webservices.gsonokhttp.ui.viewmodels + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.krunal.demo.utils.AppConstants +import com.krunal.demo.utils.AppConstants.TAG +import com.krunal.demo.webservices.gsonokhttp.data.models.api.Movie +import com.krunal.demo.webservices.gsonokhttp.data.models.api.UserDetail +import com.krunal.demo.webservices.gsonokhttp.data.models.local.UserRegistration +import com.krunal.demo.webservices.gsonokhttp.repositories.UserRepository +import com.krunal.demo.webservices.utils.Resource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +class UserRegisterViewModel : ViewModel() { + + val userDetails: MutableStateFlow = MutableStateFlow(UserRegistration()) + + private val _isUpdateUser: MutableStateFlow = MutableStateFlow(false) + val isUpdateUser: StateFlow = _isUpdateUser + + private val USER_URL = "${AppConstants.MOCKAPI_BASE_URL}/users" + + fun submitDetails() { + if (isUpdateUser.value) { + updateUser() + } else { + registerUser() + } + } + + private fun registerUser() { + viewModelScope.launch { + userDetails.value.let { details -> + val movies = details.movies?.split(",")?.map { + Movie(it.trim()) + } + + val userDetail = UserDetail( + userId = details.userId, + name = details.name, + email = details.email, + dob = details.dob, + favoriteMovies = movies ?: emptyList() + ) + UserRepository.updateUser(userDetail) + } + } + } + + private fun updateUser() { + viewModelScope.launch(Dispatchers.IO) { + userDetails.value.let { details -> + val movies = details.movies?.split(",")?.map { + Movie(it.trim()) + } ?: emptyList() + + val userDetail = UserDetail( + userId = details.userId, + name = details.name, + email = details.email, + dob = details.dob, + favoriteMovies = movies + ) + UserRepository.registerUser(userDetail) + } + } + } + + fun setUpdateUser(userId: Int) { + viewModelScope.launch { + _isUpdateUser.emit(true) + UserRepository.getUser(userId).collectLatest { response -> + when (response) { + is Resource.Loading -> {} + + is Resource.Success -> { + response.data?.let { user -> + val userRegistration = UserRegistration(userId = user.userId, + _name = user.name, + _email = user.email, + _dob = user.dob, + _movies = user.favoriteMovies.joinToString(", ") { it.name }) + userDetails.emit(userRegistration) + } + } + + is Resource.Error -> {} + } + } + } + } + + fun deleteUser(onDelete: (successful: Boolean) -> Unit) { + val url = "$USER_URL/${userDetails.value.userId}" + val client = OkHttpClient.Builder().build() + val request = Request.Builder().url(url).delete().build() + + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + CoroutineScope(Job() + Dispatchers.Main).launch { + onDelete(false) + } + Log.e(TAG, "onResponse: ${e.localizedMessage}") + } + + override fun onResponse(call: Call, response: Response) { + CoroutineScope(Job() + Dispatchers.Main).launch { + onDelete(response.isSuccessful) + } + Log.i(TAG, "onResponse: ${response.message}") + } + }) + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserViewModel.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserViewModel.kt new file mode 100644 index 0000000..fa8b598 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/gsonokhttp/ui/viewmodels/UserViewModel.kt @@ -0,0 +1,47 @@ +package com.krunal.demo.webservices.gsonokhttp.ui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.krunal.demo.webservices.gsonokhttp.data.models.api.UserDetail +import com.krunal.demo.webservices.gsonokhttp.repositories.UserRepository +import com.krunal.demo.webservices.utils.Resource +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class UserViewModel : ViewModel() { + + private val _users: MutableStateFlow> = MutableStateFlow(emptyList()) + val users = _users + + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading + + init { + loadData() + } + + private fun loadData() { + viewModelScope.launch { + + UserRepository.getAllUsers().collectLatest { response -> + when (response) { + is Resource.Loading -> { + + _isLoading.emit(true) + } + + is Resource.Success -> { + response.data?.let { _users.emit(it) } + _isLoading.emit(false) + } + + is Resource.Error -> { + _isLoading.emit(false) + } + } + } + + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/utils/Resource.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/utils/Resource.kt new file mode 100644 index 0000000..7ed879f --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/utils/Resource.kt @@ -0,0 +1,8 @@ +package com.krunal.demo.webservices.utils + +sealed class Resource(val data: T? = null, val message: String? = null) { + + class Loading(data: T? = null) : Resource(data) + class Success(data: T) : Resource(data) + class Error(message: String, data: T? = null) : Resource(data, message) +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt index ec34063..c7e0ced 100644 --- a/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt +++ b/Demo/app/src/main/java/com/krunal/demo/webservices/withoutlibrary/data/apis/NewsApi.kt @@ -13,7 +13,7 @@ import java.net.URL object NewsApi { private const val HEADLINE_URL = - "${AppConstants.NEST_BASE_URL}/top-headlines?country=in&apiKey=${AppConstants.NEWS_API_KEY}" + "${AppConstants.NEWS_BASE_URL}/top-headlines?country=in&apiKey=${AppConstants.NEWS_API_KEY}" suspend fun getNews(): NewsResponse? { var newsResponse: NewsResponse? = null diff --git a/Demo/app/src/main/res/layout/activity_main.xml b/Demo/app/src/main/res/layout/activity_main.xml index 4b9c1e0..13c376c 100644 --- a/Demo/app/src/main/res/layout/activity_main.xml +++ b/Demo/app/src/main/res/layout/activity_main.xml @@ -17,7 +17,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/app/src/main/res/layout/fragment_user_list.xml b/Demo/app/src/main/res/layout/fragment_user_list.xml new file mode 100644 index 0000000..f555f5e --- /dev/null +++ b/Demo/app/src/main/res/layout/fragment_user_list.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/app/src/main/res/layout/fragment_user_register.xml b/Demo/app/src/main/res/layout/fragment_user_register.xml new file mode 100644 index 0000000..483d29c --- /dev/null +++ b/Demo/app/src/main/res/layout/fragment_user_register.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/app/src/main/res/layout/item_user_detail.xml b/Demo/app/src/main/res/layout/item_user_detail.xml new file mode 100644 index 0000000..0b19558 --- /dev/null +++ b/Demo/app/src/main/res/layout/item_user_detail.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Demo/app/src/main/res/menu/search_web_bottom_nav.xml b/Demo/app/src/main/res/menu/search_web_bottom_nav.xml index 45a232f..6e21ae8 100644 --- a/Demo/app/src/main/res/menu/search_web_bottom_nav.xml +++ b/Demo/app/src/main/res/menu/search_web_bottom_nav.xml @@ -15,5 +15,5 @@ android:id="@+id/registerFragment" android:contentDescription="@string/cd_form" android:icon="@drawable/ic_feedback" - android:title="@string/title_register" /> + android:title="@string/register" /> \ No newline at end of file diff --git a/Demo/app/src/main/res/menu/trivia_game_bottom_nav.xml b/Demo/app/src/main/res/menu/trivia_game_bottom_nav.xml index 258eb2a..a10cada 100644 --- a/Demo/app/src/main/res/menu/trivia_game_bottom_nav.xml +++ b/Demo/app/src/main/res/menu/trivia_game_bottom_nav.xml @@ -15,5 +15,5 @@ android:id="@+id/registerFragment" android:contentDescription="@string/cd_form" android:icon="@drawable/ic_feedback" - android:title="@string/title_register" /> + android:title="@string/register" /> \ No newline at end of file diff --git a/Demo/app/src/main/res/values/dimens.xml b/Demo/app/src/main/res/values/dimens.xml index f5336f7..280077f 100644 --- a/Demo/app/src/main/res/values/dimens.xml +++ b/Demo/app/src/main/res/values/dimens.xml @@ -43,4 +43,6 @@ 15sp 4dp 130dp + 36dp + 56dp \ No newline at end of file diff --git a/Demo/app/src/main/res/values/strings.xml b/Demo/app/src/main/res/values/strings.xml index 2e7d03a..5ea672c 100644 --- a/Demo/app/src/main/res/values/strings.xml +++ b/Demo/app/src/main/res/values/strings.xml @@ -196,7 +196,7 @@ Bottom navigation list List Bottom navigation registration form - Register + Register Rank # %1$d Losses @@ -225,4 +225,13 @@ Search app or package name Toggle Dark Mode Validate + Full Name + Favorite Movies + Harry Potter, Stranger Things, Mr. Robot + Submit + Delete + Are you sure to delete? + OK + Successfully deleted user %1$s + Can\'t delete user %1$s \ No newline at end of file