MVVM - Boilerplate
Type Converter
class Converter {
@TypeConverter
fun fromListOfStringsToString(value: List<String>): String {
val gson = Gson()
val type = object : TypeToken<List<String>>() {}.type
return gson.toJson(value, type)
}
@TypeConverter
fun fromStringToListOfStrings(value: String): List<String> {
val gson = Gson()
val type = object : TypeToken<List<String>>() {}.type
return gson.fromJson(value, type)
}
}
Network Module
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideService(retrofit: Retrofit): PostApi {
return retrofit.create(PostApi::class.java)
}
}
Handle Internet Connection
private fun hasInternetConnection(): Boolean {
val connectivityManager =
getApplication<Application>().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
return when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
}
Check Response Data
private fun handleProductResponse(response: Response<Product>): NetworkResult<Product>? {
when {
response.message().contains("timeout") -> {
return NetworkResult.Error("TimeOut")
}
response.code() == 402 -> {
return NetworkResult.Error("API Key Limited")
}
response.body()!!.results.isEmpty() -> {
return NetworkResult.Error("Product not found")
}
response.isSuccessful -> {
return NetworkResult.Success(response.body()!!)
}
else -> {
return NetworkResult.Error(response.message())
}
}
}
Coroutine Scope And Exception Handler
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = mainRepository.getAllMovies()
}
AsynListDiffer - DifferCallback
private val differCallback = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
Network Bound Resource
inline fun <T, K> networkBoundResource(
crossinline query: () -> Flow<T>,
crossinline fetch: suspend () -> K,
crossinline saveFetchResult: suspend (K) -> Unit,
crossinline shouldFetch: (T) -> Boolean = { true }
) = channelFlow<Resource<*>> {
val data = query().first()
send(Resource.loading(null))
if (shouldFetch(data)) {
try {
saveFetchResult(fetch())
try {
query().collect { send(Resource.success(it)) }
} catch (e: Exception) {
send(Resource.error(data = null, message = e.localizedMessage ?: "Error!"))
}
} catch (e: Exception) {
try {
query().collect {
send(
Resource.error(
data = null,
message = e.localizedMessage ?: "Error $it"
)
)
}
} catch (e: Exception) {
send(Resource.error(data = null, message = e.localizedMessage ?: "Error"))
}
}
} else {
try {
query().collect { send(Resource.success(it)) }
} catch (e: Exception) {
send(Resource.error(data = null, message = e.localizedMessage ?: "Error"))
}
}
}
ViewBinding Delegate
/** Activity binding delegate, may be used since onCreate up to onDestroy (inclusive) */
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline factory: (LayoutInflater) -> T) =
lazy(LazyThreadSafetyMode.NONE) {
factory(layoutInflater)
}
/** Fragment binding delegate, may be used since onViewCreated up to onDestroyView (inclusive) */
fun <T : ViewBinding> Fragment.viewBinding(factory: (View) -> T): ReadOnlyProperty<Fragment, T> =
object : ReadOnlyProperty<Fragment, T>, DefaultLifecycleObserver {
private var binding: T? = null
override fun getValue(thisRef: Fragment, property: KProperty<*>): T =
binding ?: factory(requireView()).also {
// if binding is accessed after Lifecycle is DESTROYED, create new instance, but don't cache it
if (viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
viewLifecycleOwner.lifecycle.addObserver(this)
binding = it
}
}
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
}
/** Binding delegate for DialogFragments implementing onCreateDialog (like Activities, they don't
* have a separate view lifecycle), may be used since onCreateDialog up to onDestroy (inclusive) */
inline fun <T : ViewBinding> DialogFragment.viewBinding(crossinline factory: (LayoutInflater) -> T) =
lazy(LazyThreadSafetyMode.NONE) {
factory(layoutInflater)
}
/** Not really a delegate, just a small helper for RecyclerView.ViewHolders */
inline fun <T : ViewBinding> ViewGroup.viewBinding(factory: (LayoutInflater, ViewGroup, Boolean) -> T) =
factory(LayoutInflater.from(context), this, false)
Network Resource
sealed class Resource<T> {
data class Success<T>(val data: T) : Resource<T>()
data class Error<T>(val exception: Throwable) : Resource<T>()
data class Loading<T>(val data: T? = null) : Resource<T>()
fun <R> mapData(transform: (T) -> R): Resource<R> = when (this) {
is Success -> Success(
transform(data)
)
is Error -> Error(
exception
)
is Loading -> Loading(
data?.let { transform(it) }
)
}
}
sealed class Status {
object Content : Status()
data class Error(val exception: Throwable) : Status()
object Loading : Status()
object ContentWithLoading : Status()
}
class ResourceError(
@StringRes
val actionText: Int,
@StringRes
val errorTitle: Int,
@StringRes
val errorDesc: Int
)
class NoContentListingException : Exception()
object ErrorResolver {
fun resolve(throwable: Throwable): ResourceError {
return when (throwable) {
is NoContentListingException -> ResourceError(
actionText = R.string.no_content_error_action,
errorDesc = R.string.no_content_error_message,
errorTitle = R.string.default_error_title
)
else ->
ResourceError(
actionText = R.string.common_action_retry,
errorDesc = R.string.default_error_message,
errorTitle = R.string.default_error_title
)
}
}
}
class StatusViewState(private val status: Status) {
fun getStateInfo(context: Context): StateLayout.StateInfo {
return when (status) {
Status.Content -> StateLayout.provideContentStateInfo()
is Status.Error -> provideErrorState(context, status.exception)
Status.Loading -> StateLayout.provideLoadingStateInfo()
Status.ContentWithLoading -> StateLayout.provideLoadingWithContentStateInfo()
}
}
private fun provideErrorState(context: Context, exception: Throwable): StateLayout.StateInfo {
val resourceError = ErrorResolver.resolve(exception)
return StateLayout.StateInfo(
infoImage = R.drawable.state_info,
infoTitle = context.getString(resourceError.errorTitle),
infoMessage = context.getString(resourceError.errorDesc),
infoButtonText = context.getString(resourceError.actionText),
state = StateLayout.State.ERROR
)
}
}
Last updated