Thursday 12 September 2024

Sealed class | MVVM | Retrofit | RecyclerView | Recycler Adapter | Coroutines Flow | Image Loader in Adapter

 1. RetrofitService

object RetrofitService {
private const val URL = "https://jsonplaceholder.typicode.com/"
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}

private val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()

fun getRetroService(): ApiService {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(URL)
.build().create(ApiService::class.java)
}
}
2. ApiService
interface ApiService {
@GET("photos")
suspend fun getPhoto():List<PhotoResponseItem>
}
3. ApiStateController
sealed class ApiStateController {
object Loading : ApiStateController()
object Empty : ApiStateController()
data class Success(val data: List<PhotoResponseItem>) : ApiStateController()
data class Failure(val msg: String) : ApiStateController()
}
4. PhotoRepository
class PhotoRepository(private val apiService: ApiService) {

fun getPhoto():Flow<List<PhotoResponseItem>> = flow {
emit(apiService.getPhoto())
}.flowOn(Dispatchers.IO)
}
5.PhotoViewModel
class PhotoViewModel(private val photoRepository: PhotoRepository) : ViewModel() {
private val _photo: MutableStateFlow<ApiStateController> =
MutableStateFlow(ApiStateController.Empty)
val photo: StateFlow<ApiStateController>
get() = _photo

fun getPhoto() {
viewModelScope.launch {
_photo.value = ApiStateController.Loading
photoRepository.getPhoto()
.catch { e ->
_photo.value = ApiStateController.Failure(e.message.toString())
}.collect { data ->
_photo.value = ApiStateController.Success(data)
}
}
}
}
6.PhotoViewModelFactory
class PhotoViewModelFactory(private val photoRepository: PhotoRepository) :
ViewModelProvider.NewInstanceFactory() {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
return PhotoViewModel(photoRepository) as T
}
}
7.PhotoActivity
class PhotoActivity : AppCompatActivity() {
private lateinit var photoViewModel: PhotoViewModel
private lateinit var binding: ActivityPhotoBinding
private lateinit var photoAdapter: PhotoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_photo)
initRecyclerView()

val api = RetrofitService.getRetroService()
val repository = PhotoRepository(api)
val factory = PhotoViewModelFactory(repository)
photoViewModel = ViewModelProvider(this, factory)[PhotoViewModel::class.java]
photoViewModel.getPhoto()
getPhoto()
}

private fun getPhoto() {
lifecycle.coroutineScope.launchWhenStarted {
photoViewModel.photo.collect { it ->
when (it) {
ApiStateController.Empty -> {
Log.d("Response====", "===Empty=====")

}

is ApiStateController.Failure -> {
Log.d("Response====", "===Error:= ${it.msg}=====")

binding.progress.visibility = View.GONE
Snackbar.make(binding.root, it.msg, Snackbar.LENGTH_SHORT).show()

}

ApiStateController.Loading -> {
binding.progress.visibility = View.VISIBLE
Log.d("Response====", "===Loading=====")
}

is ApiStateController.Success -> {
binding.progress.visibility = View.GONE
Log.d("Response is===", "${it.data}")
photoAdapter.setData(it.data)
photoAdapter.notifyDataSetChanged()

}
}
}
}
}

private fun initRecyclerView() {
photoAdapter = PhotoAdapter(ArrayList())
binding.recyclerView.apply {
setHasFixedSize(true)
adapter = photoAdapter
layoutManager = LinearLayoutManager(this@PhotoActivity)
}
}

}
8. PhotoAdapter
class PhotoAdapter(private var photoList: List<PhotoResponseItem>) :
RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder>() {
lateinit var binding: EachRowBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
binding = EachRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PhotoViewHolder(binding.root)
}

override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
binding.id.text = photoList[position].id.toString()
binding.title.text = photoList[position].title
Glide
.with(binding.thumbnailUrl)
.load(photoList[position].thumbnailUrl)
.centerCrop()
.into(binding.thumbnailUrl);

}

override fun getItemCount(): Int {
return photoList.size
}

class PhotoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}

fun setData(photoData: List<PhotoResponseItem>) {
this.photoList = photoData
notifyDataSetChanged()
}


}
9. photo_activity
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>

</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="30dp"
android:visibility="gone" />

</RelativeLayout>
</layout>

10. each_row.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>

</data>

<androidx.cardview.widget.CardView xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="8dp"
android:orientation="vertical"
app:cardCornerRadius="8dp"
app:cardUseCompatPadding="true">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/thumbnailUrl"
android:layout_width="100dp"
android:layout_height="100dp" />

<LinearLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/thumbnailUrl"
android:orientation="vertical">

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="Hello world" />

<TextView
android:id="@+id/id"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp"
tools:text="1" />

</LinearLayout>

</RelativeLayout>


</androidx.cardview.widget.CardView>
</layout>

Wednesday 11 September 2024

Simple Example of Sealed class using ViewModel

 1.  MyViewModel

MyViewModel

class MyViewModel : ViewModel() {
private val _state = MutableStateFlow<StateController>(StateController.Empty)
val state: StateFlow<StateController>
get() = _state

fun login(name: String, pass: String) {
_state.value = StateController.Loading
viewModelScope.launch {
delay(2000)

if (name == "simi" && pass == "123") {
_state.value = StateController.Success("Authenticated")
} else {
_state.value = StateController.Error("Invalid UserID & Password")
}
}
}
}
2. StateController
StateController
package com.example.sealedviewmodelex

sealed class StateController {
data object Loading : StateController()
data object Empty : StateController()
data class Success(val data: String) : StateController()
data class Error(val msg: String) : StateController()

}
3. MainActivity
MAinActivity
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MyViewModel
lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
binding.clickMe.setOnClickListener {
viewModel.login(binding.username.text
.toString(), binding.password.text.toString())
}
lifecycle.coroutineScope.launchWhenCreated {
viewModel.state.collect {
when (it) {
is StateController.Error -> {
binding.progress.visibility=View.GONE
Snackbar.make(binding.root, it.msg, Snackbar.LENGTH_SHORT).show()
}

is StateController.Loading -> {
binding.progress.visibility = View.VISIBLE
}

is StateController.Success -> {
binding.progress.visibility=View.GONE
Snackbar.make(binding.root, "Authenticated", Snackbar.LENGTH_SHORT).show()
}

StateController.Empty -> {

}
}
}
}
}
}

4. Dependencies
libs class:
lifecycle_version = "2.8.4"
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", 
name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle_version" }
buildFeatures{
dataBinding=true
}
implementation(libs.androidx.lifecycle.viewmodel.ktx)

Thursday 29 August 2024

View Image url from api to ImageView in RecyclerView Adapter class Using DataBinding

 1. build.gradle.kts

dependencies{
//image  loader
implementation(libs.androidx.glide.bumptech)
}
plugins {
id("org.jetbrains.kotlin.kapt")
}
2. libs.version.toml
androidx-glide-bumptech={group="com.github.bumptech.glide",name="glide",version="4.16.0"}
3.Create a databinding utils class like below:
package com.example.mvvmtest3

import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
import java.net.URL

@BindingAdapter("android:bindImage")
fun loadImage(view: ImageView, url: String) {
Glide.with(view)
.load(url)
.into(view)
}
4. recycler_list.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>

<variable
name="album"
type="com.example.mvvmtest3.model.AlbumResponseItem" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
    android:layout_width="100dp"
android:bindImage="@{album.title}"
android:id="@+id/imageView"
android:contentDescription="@string/app_name"
android:layout_height="100dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_toRightOf="@+id/imageView"
android:orientation="vertical">

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@{album.title}"
android:textColor="@color/black"
android:textStyle="bold" />

<TextView
android:id="@+id/id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(album.id)}" />

<TextView
android:id="@+id/userId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@{String.valueOf(album.userId)}" />

</LinearLayout>

</RelativeLayout>
</androidx.cardview.widget.CardView>

</LinearLayout>
</layout>

View Image url from api to ImageView in RecyclerView Adapter class

1. PhotoAdapter.kt

override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
val photoList = photoResponceItem[position]
holder.title.text = photoList.title
holder.id.text = photoList.id.toString()
Glide
.with(holder.imageView)
.load(photoList.thumbnailUrl)
.centerCrop()
.into(holder.imageView);
}
inner class PhotoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.title)
val imageView: ImageView =itemView.findViewById(R.id.imageView)
}
2. build.gradle.kts
dependencies{
//image  loader
implementation(libs.androidx.glide.bumptech)
}
plugins {
id("org.jetbrains.kotlin.kapt")
}
3. libs.version.toml
androidx-glide-bumptech={group="com.github.bumptech.glide",name="glide",version="4.16.0"}

Monday 26 August 2024

Kotlin | MVVM | API call Retrofit | viewModel | repository class |RecyclerView example

 

1. First add dependency in  the app  gradle file : build.gradle.kts(:app)

android {
dataBinding {
enable = true
}
buildFeatures {
viewBinding = true
}
}
dependencies {
//navigation fragment
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)

//coroutines
implementation(libs.androidx.coroutines.core.ktx)
implementation(libs.androidx.coroutines.ktx)

// Retrofit
implementation(libs.androidx.retrofit2.converter.gson)
implementation(libs.androidx.retrofit2)
implementation(libs.androidx.okhttp3.logging.interceptor)

//view model
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
//New Material Design
implementation(libs.androidx.android.material.ktx)
implementation(libs.androidx.arch.lifecycle.viewmodel)
// ViewModel and LiveData
implementation(libs.androidx.lifecycle)
}

2. libs.versions.toml

coroutinesVersion = "1.7.1"
retrofit = "2.9.0"
gson = "2.6.2"
okhttp3 = "4.5.0"
lifecycle_version = "2.8.4"
material_version = "1.12.0"
legacySupportV4 = "1.0.0"
fragmentKtx = "1.8.2"
navigationFragmentKtx = "2.7.7"
navigationUiKtx = "2.7.7"
androidx-coroutines-core-ktx = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
androidx-coroutines-ktx = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutinesVersion" }

androidx-retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
androidx-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "gson" }
androidx-okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp3" }

androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle_version" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle_version" }
androidx-android-material-ktx={group="com.google.android.material",name="material",version.ref="material_version"}
androidx-arch-lifecycle-viewmodel={group="android.arch.lifecycle",name="viewmodel",version="1.1.1"}
androidx-lifecycle={group="androidx.lifecycle",name="lifecycle-extensions",version="2.2.0"}
androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

3. gradle.properties 

android.databinding.enableV2=true

4. MyApi.kt   interface

interface MyApi {
@GET("albums")
suspend fun getAlbum(): Response<List<AlbumResponseItem>>

companion object {
operator fun invoke(): MyApi {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://jsonplaceholder.typicode.com/")
.build()
.create(MyApi::class.java)
}
}

}

5. SafeApiRequest.kt   abstract class

abstract class SafeApiRequest {
//generic function T of type Any :: apiRequest -is function name
suspend fun <T : Any> apiRequest(apiCall: suspend () -> Response<T>): T {
val response = apiCall.invoke()
if (response.isSuccessful) {
return response.body()!!
} else {
response.code()
throw apiException(response.code().toString())
}

}
}

class apiException(msg: String) : IOException(msg)

6. AlbumResponseItem.kt   data class

data class AlbumResponseItem(
val id: Int,
val title: String,
val userId: Int
)

7. AlbumRepository.kt   class


class AlbumRepository(private val api: MyApi):SafeApiRequest() {
suspend fun getAlbumRepo()= apiRequest { api.getAlbum() }
}

8. AlbumViewModel.kt   class

class AlbumViewModel(private val albumRepository: AlbumRepository) : ViewModel() {
private lateinit var job: Job
private val albumMutableLiveData = MutableLiveData<List<AlbumResponseItem>>()
val albumLiveData: LiveData<List<AlbumResponseItem>>
get() = albumMutableLiveData

fun getAlbumViewModel() {
job = Coroutines.ioMainThreadCall({ albumRepository.getAlbumRepo() },
{ albumMutableLiveData.value = it })
}
override fun onCleared() {
super.onCleared()
if (::job.isInitialized)
job.cancel()
}
}

9. AlbumViewModelFactory.kt   class

@Suppress("UNCHECKED_CAST")
class AlbumViewModelFactory(private val albumRepository: AlbumRepository) :
ViewModelProvider.NewInstanceFactory() {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AlbumViewModel(albumRepository) as T
}
}

10. AlbumAdapter.kt   class

package com.example.mvvmretrofit.utils

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.example.mvvmretrofit.R
import com.example.mvvmretrofit.databinding.ItemListBinding
import com.example.mvvmretrofit.model.AlbumResponseItem

class AlbumAdaper(private val albumResponseItem: List<AlbumResponseItem>) :
RecyclerView.Adapter<AlbumAdaper.MyViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MyViewHolder(
DataBindingUtil.inflate<ItemListBinding>(
LayoutInflater.from(parent.context),
R.layout.item_list,
parent,
false
)
)

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.itemListBinding.albumResItem=albumResponseItem[position]
}

override fun getItemCount(): Int {
return albumResponseItem.size
}

inner class MyViewHolder(val itemListBinding: ItemListBinding) :
RecyclerView.ViewHolder(itemListBinding.root)
}

11. Coroutines.kt   object class

object Coroutines {
/*Generic function*/
fun <T : Any> ioMainThreadCall(work: suspend () -> T, callback: ((T?) -> Unit)) =
CoroutineScope(Dispatchers.Main).launch {
val data = CoroutineScope(Dispatchers.IO).async {
return@async work()
}.await()
callback(data)

}
}

12. AlbumFragment.kt   interface

class AlbumFragment : Fragment() {

lateinit var viewModel: AlbumViewModel
lateinit var factory: AlbumViewModelFactory
lateinit var binding: FragmentAlbumBinding

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentAlbumBinding.inflate(layoutInflater)
return binding.root
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val myApi = MyApi()
val repository = AlbumRepository(myApi)
factory = AlbumViewModelFactory(repository)
//viewModel = ViewModelProviders.of(this, factory).get(AlbumViewModel::class.java)
viewModel = ViewModelProvider(this, factory)[AlbumViewModel::class.java]
viewModel.getAlbumViewModel()
viewModel.albumLiveData.observe(viewLifecycleOwner, Observer {albumList->
binding.albumRecyclerView.also {
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
it.adapter = AlbumAdaper(albumList)
}
})

}
}

13. MainActivity.kt   class

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

14. activity_main.xml   

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">


<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView2"
android:name="com.example.mvvmretrofit.view.AlbumFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

15. fragment_album.kt  

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.AlbumFragment">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/albumRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</FrameLayout>

16. item_list.xml   

<?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">

<data>

<variable
name="albumResItem"
type="com.example.mvvmretrofit.model.AlbumResponseItem" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_margin="10dp"
app:cardCornerRadius="5dp"
android:layout_height="wrap_content">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="vertical">

<TextView
android:id="@+id/id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(albumResItem.id)}" />

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{albumResItem.title}" />

<TextView
android:id="@+id/userId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(albumResItem.userId)}" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>