diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..526b4c2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f8b3a95 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..441a449 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,108 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + // Kotlin Kapt + id 'kotlin-kapt' + // Parcelable + id("kotlin-parcelize") + // Hilt + id("dagger.hilt.android.plugin") + // Navigation Safe Args + id("androidx.navigation.safeargs.kotlin") +} + +android { + compileSdk 31 + + defaultConfig { + applicationId "com.example.rickandmortyapp" + minSdk 21 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + viewBinding true + } +} + +dependencies { + + // Kotlin + // | Stdlib + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + + // Kotlin Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") + + // Core + implementation 'androidx.core:core-ktx:1.7.0' + + // Appcompat + implementation 'androidx.appcompat:appcompat:1.3.0' + + // Material Design Components + implementation 'com.google.android.material:material:1.4.0' + + // UI Components + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + + // Hilt + implementation("com.google.dagger:hilt-android:$hilt_version") + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + kapt("com.google.dagger:hilt-android-compiler:$hilt_version") + + // Navigation Components + def nav_version = "2.3.5" + implementation("androidx.navigation:navigation-fragment-ktx:$nav_version") + implementation("androidx.navigation:navigation-ui-ktx:$nav_version") + + // Retrofit 2 + def retrofit_version = "2.9.0" + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + // Gson + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + + // OkHttp + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.0")) + + // define any required OkHttp artifacts without version + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + // Activity version + def activity_version = "1.3.0" + implementation("androidx.activity:activity-ktx:$activity_version") + + // Fragment version + def fragment_version = "1.3.6" + implementation("androidx.fragment:fragment-ktx:$fragment_version") + + // ViewBindingPropertyDelegate + def view_binding_property_delegate = '1.5.3' + // To use only without reflection variants of viewBinding + implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:$view_binding_property_delegate" + + // Glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + + // Paging 3 + def paging_version = "3.0.1" + implementation("androidx.paging:paging-runtime-ktx:$paging_version") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/rickandmortyapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/rickandmortyapp/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1b64d2a --- /dev/null +++ b/app/src/androidTest/java/com/example/rickandmortyapp/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.rickandmortyapp + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.rickandmortyapp", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0582726 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/App.kt b/app/src/main/java/com/example/rickandmortyapp/App.kt new file mode 100644 index 0000000..7ab09e0 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/App.kt @@ -0,0 +1,7 @@ +package com.example.rickandmortyapp + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/base/BaseDiffUtilItemCallback.kt b/app/src/main/java/com/example/rickandmortyapp/base/BaseDiffUtilItemCallback.kt new file mode 100644 index 0000000..6645e6e --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/BaseDiffUtilItemCallback.kt @@ -0,0 +1,14 @@ +package com.example.rickandmortyapp.base + +import androidx.recyclerview.widget.DiffUtil + +class BaseDiffUtilItemCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/base/BaseFragment.kt b/app/src/main/java/com/example/rickandmortyapp/base/BaseFragment.kt new file mode 100644 index 0000000..d8b3196 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/BaseFragment.kt @@ -0,0 +1,39 @@ +package com.example.rickandmortyapp.base + +import android.os.Bundle +import android.view.View +import androidx.annotation.LayoutRes +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding + +abstract class BaseFragment( + @LayoutRes layoutId: Int +) : Fragment(layoutId) { + + protected abstract val binding: BaseViewBinding + protected abstract val viewModel: ViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initialize() + setupViews() + setupListeners() + setupRequests() + setupObserves() + } + + open fun initialize() { + } + + open fun setupViews() { + } + + open fun setupListeners() { + } + + open fun setupRequests() { + } + + open fun setupObserves() { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/base/BasePagingSource.kt b/app/src/main/java/com/example/rickandmortyapp/base/BasePagingSource.kt new file mode 100644 index 0000000..0d0dbdc --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/BasePagingSource.kt @@ -0,0 +1,45 @@ +package com.example.rickandmortyapp.base + +import android.net.Uri +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.bumptech.glide.load.HttpException +import com.example.rickandmortyapp.data.network.dtos.models.ApiResponse +import java.io.IOException + +abstract class BasePagingSource( + private val service: suspend (nextPage: Int) -> ApiResponse, + private val mappedData: (data: List) -> List +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val nextPage = params.key ?: 1 + val response = service(nextPage) + val next = response.info.next + val nextPageNumber = if (next == null) { + null + } else { + Uri.parse(next).getQueryParameter("page")?.toInt() + } + LoadResult.Page( + data = mappedData(response.results), + prevKey = null, + nextKey = nextPageNumber + ) + } catch (http: HttpException) { + LoadResult.Error(http) + } catch (e: IOException) { + LoadResult.Error(e) + } catch (exception: Exception) { + LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey + } + } +} diff --git a/app/src/main/java/com/example/rickandmortyapp/base/BaseRecyclerViewHolder.kt b/app/src/main/java/com/example/rickandmortyapp/base/BaseRecyclerViewHolder.kt new file mode 100644 index 0000000..e236f74 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/BaseRecyclerViewHolder.kt @@ -0,0 +1,11 @@ +package com.example.rickandmortyapp.base + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +abstract class BaseRecyclerViewHolder( + val binding: V +) : RecyclerView.ViewHolder(binding.root) { + + abstract fun onBind(item: I): Unit? +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/base/BaseRepository.kt b/app/src/main/java/com/example/rickandmortyapp/base/BaseRepository.kt new file mode 100644 index 0000000..4c2e1c0 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/BaseRepository.kt @@ -0,0 +1,46 @@ +package com.example.rickandmortyapp.base + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.example.rickandmortyapp.common.resource.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +abstract class BaseRepository { + + protected fun doRequest(request: suspend () -> T) = flow { + emit(Resource.Loading()) + try { + emit(Resource.Success(data = request())) + } catch (e: Exception) { + emit( + Resource.Error( + data = null, message = e.localizedMessage ?: "Error Occurred" + ) + ) + } + } + + protected fun doPagingRequest( + pagingSource: BasePagingSource, + pageSize: Int = 10, + prefetchDistance: Int = pageSize, + enabledPlaceholder: Boolean = true, + initialLoadSize: Int = pageSize * 3, + maxSize: Int = Int.MAX_VALUE, + jumpThreshold: Int = Int.MIN_VALUE, + ): Flow> { + return Pager( + config = PagingConfig( + pageSize, + prefetchDistance, + enabledPlaceholder, + initialLoadSize, + maxSize, + jumpThreshold + ), + pagingSourceFactory = { pagingSource } + ).flow + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/base/BaseViewModel.kt b/app/src/main/java/com/example/rickandmortyapp/base/BaseViewModel.kt new file mode 100644 index 0000000..c630f68 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/BaseViewModel.kt @@ -0,0 +1,38 @@ +package com.example.rickandmortyapp.base + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.rickandmortyapp.common.resource.Resource +import com.example.rickandmortyapp.presentation.state.UIState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +abstract class BaseViewModel : ViewModel() { + + protected fun subscribeTo( + state: MutableLiveData>, + request: () -> Flow> + ) { + viewModelScope.launch { + request().collect { + when (it) { + is Resource.Loading -> { + state.value = UIState.Loading() + } + is Resource.Error -> { + it.message?.let { error -> + state.value = UIState.Error(error) + } + } + is Resource.Success -> { + it.data?.let { data -> + state.value = UIState.Success(data) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/base/IBaseDiffModel.kt b/app/src/main/java/com/example/rickandmortyapp/base/IBaseDiffModel.kt new file mode 100644 index 0000000..339ba9a --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/base/IBaseDiffModel.kt @@ -0,0 +1,6 @@ +package com.example.rickandmortyapp.base + +interface IBaseDiffModel { + val id: Int? + override fun equals(other: Any?): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/constants/Constants.kt b/app/src/main/java/com/example/rickandmortyapp/common/constants/Constants.kt new file mode 100644 index 0000000..92e7d6a --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/constants/Constants.kt @@ -0,0 +1,9 @@ +package com.example.rickandmortyapp.common.constants + +object Constants { + + const val BASE_URL = "https://rickandmortyapi.com/" + const val CHARACTER_VIEW_TYPE = 0 + const val LOCATION_VIEW_TYPE = 1 + const val EPISODES_VIEW_TYPE = 2 +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/extensions/DateFormatExtension.kt b/app/src/main/java/com/example/rickandmortyapp/common/extensions/DateFormatExtension.kt new file mode 100644 index 0000000..8f84390 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/extensions/DateFormatExtension.kt @@ -0,0 +1,22 @@ +package com.example.rickandmortyapp.common.extensions + +import android.annotation.SuppressLint +import android.icu.text.SimpleDateFormat +import java.text.ParseException +import java.util.* + +@SuppressLint("SimpleDateFormat", "NewApi") +fun toFormatDate(oldStringDate: String?): String? { + if (oldStringDate == null || oldStringDate == "") + return "" + val newDate: String? + val dateFormat = SimpleDateFormat("MMMM d yyyy - mm:ss", Locale.ENGLISH) + newDate = try { + val date: Date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").parse(oldStringDate) + dateFormat.format(date) + } catch (e: ParseException) { + e.printStackTrace() + oldStringDate + } + return newDate +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/extensions/GlideExtension.kt b/app/src/main/java/com/example/rickandmortyapp/common/extensions/GlideExtension.kt new file mode 100644 index 0000000..2e7e48c --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/extensions/GlideExtension.kt @@ -0,0 +1,12 @@ +package com.example.rickandmortyapp.common.extensions + +import android.widget.ImageView +import com.bumptech.glide.Glide + +fun ImageView.loadImage(url: String?, placeHolder: Int = 0) { + Glide.with(this.context) + .load(url) + .error(placeHolder) + .into(this) +} + diff --git a/app/src/main/java/com/example/rickandmortyapp/common/extensions/KeyboardExtension.kt b/app/src/main/java/com/example/rickandmortyapp/common/extensions/KeyboardExtension.kt new file mode 100644 index 0000000..98c7ce3 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/extensions/KeyboardExtension.kt @@ -0,0 +1,33 @@ +package com.example.rickandmortyapp.common.extensions + +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.fragment.app.Fragment + +fun Activity.hideKeyboard() { + hideKeyboard(currentFocus ?: View(this)) +} + +fun Fragment.hideKeyboard() { + view?.let { activity?.hideKeyboard(it) } +} + +fun Activity.showKeyboard() { + showKeyboard(currentFocus ?: View(this)) +} + +fun Fragment.showKeyboard() { + view?.let { activity?.showKeyboard(it) } +} + +fun Context.hideKeyboard(view: View) { + val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) +} + +fun Context.showKeyboard(view: View) { + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/extensions/LiveDataExtension.kt b/app/src/main/java/com/example/rickandmortyapp/common/extensions/LiveDataExtension.kt new file mode 100644 index 0000000..6b53cd5 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/extensions/LiveDataExtension.kt @@ -0,0 +1,27 @@ +package com.example.rickandmortyapp.common.extensions + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +fun zipLiveData(a: LiveData, b: LiveData): LiveData> { + return MediatorLiveData>().apply { + var lastA: A? = null + var lastB: B? = null + + fun update() { + val localLastA = lastA + val localLastB = lastB + if (localLastA != null && localLastB != null) + this.value = Pair(localLastA, localLastB) + } + + addSource(a) { + lastA = it + update() + } + addSource(b) { + lastB = it + update() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/extensions/SearchViewExtension.kt b/app/src/main/java/com/example/rickandmortyapp/common/extensions/SearchViewExtension.kt new file mode 100644 index 0000000..665d901 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/extensions/SearchViewExtension.kt @@ -0,0 +1,42 @@ +package com.example.rickandmortyapp.common.extensions + +import android.content.Context +import android.graphics.Color +import android.view.MenuItem +import android.widget.SearchView +import com.example.rickandmortyapp.R +import kotlinx.coroutines.Job + +fun SearchView.submitSearch(getFilterByName: (query: String?) -> Job) { + this.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + getFilterByName(query) + return false + } + + override fun onQueryTextChange(query: String?): Boolean { + getFilterByName(query) + return false + } + }) +} + +fun SearchView.setTools(context: Context?) { + this.queryHint = context?.getString(R.string.searching) + this.setBackgroundColor(Color.parseColor("#0C0D0E")) +} + +fun MenuItem.setOnActionExpandListener(searchView: SearchView, hideKeyboard: () -> Unit) { + this.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(p0: MenuItem?): Boolean { + searchView.isIconified = false + searchView.onActionViewExpanded() + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + hideKeyboard() + return true + } + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/extensions/SingleClickListenerExtension.kt b/app/src/main/java/com/example/rickandmortyapp/common/extensions/SingleClickListenerExtension.kt new file mode 100644 index 0000000..9092497 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/extensions/SingleClickListenerExtension.kt @@ -0,0 +1,17 @@ +package com.example.rickandmortyapp.common.extensions + +import android.view.View +import android.widget.SearchView +import com.example.rickandmortyapp.common.singlelistener.OnSingleClickListener + +fun SearchView.setOnSingleClickListener(l: View.OnFocusChangeListener) { + setOnQueryTextFocusChangeListener(OnSingleClickListener(l)) +} + +fun SearchView.setOnSingleClickListener(l: (View) -> Unit) { + setOnQueryTextFocusChangeListener(OnSingleClickListener(l)) +} + +fun View.setOnSingleClickListener(l: (View) -> Unit) { + setOnClickListener(OnSingleClickListener(l)) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/resource/Resource.kt b/app/src/main/java/com/example/rickandmortyapp/common/resource/Resource.kt new file mode 100644 index 0000000..d436a8d --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/resource/Resource.kt @@ -0,0 +1,10 @@ +package com.example.rickandmortyapp.common.resource + +sealed class Resource( + val data: T? = null, + val message: String? = null, +) { + class Loading(data: T? = null) : Resource(data = data) + class Success(data: T) : Resource(data = data) + class Error(message: String, data: T? = null) : Resource(data = data, message = message) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/common/singlelistener/OnSingleClickListener.kt b/app/src/main/java/com/example/rickandmortyapp/common/singlelistener/OnSingleClickListener.kt new file mode 100644 index 0000000..281b599 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/common/singlelistener/OnSingleClickListener.kt @@ -0,0 +1,48 @@ +package com.example.rickandmortyapp.common.singlelistener + +import android.view.View + +class OnSingleClickListener : View.OnFocusChangeListener, View.OnClickListener { + + private lateinit var onClickListener: View.OnClickListener + + private var onFocusChangeListener: View.OnFocusChangeListener + + constructor(listener: View.OnFocusChangeListener) { + onFocusChangeListener = listener + } + + constructor(listener: (View) -> Unit) { + onFocusChangeListener = View.OnFocusChangeListener { it, hasFocus -> + if (hasFocus) { + listener.invoke(it) + } + } + onClickListener = View.OnClickListener { listener.invoke(it) } + } + + + override fun onFocusChange(v: View?, h: Boolean) { + val currentTimeMillis = System.currentTimeMillis() + + if (currentTimeMillis >= previousClickTimeMillis + DELAY_MILLIS) { + previousClickTimeMillis = currentTimeMillis + onFocusChangeListener.onFocusChange(v, h) + } + } + + override fun onClick(v: View?) { + val currentTimeMillis = System.currentTimeMillis() + + if (currentTimeMillis >= previousClickTimeMillis + DELAY_MILLIS) { + previousClickTimeMillis = currentTimeMillis + onClickListener.onClick(v) + } + } + + companion object { + private const val DELAY_MILLIS = 200L + private var previousClickTimeMillis = 0L + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/implementation/CharactersRepositoryImpl.kt b/app/src/main/java/com/example/rickandmortyapp/data/implementation/CharactersRepositoryImpl.kt new file mode 100644 index 0000000..59f43db --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/implementation/CharactersRepositoryImpl.kt @@ -0,0 +1,15 @@ +package com.example.rickandmortyapp.data.implementation + +import com.example.rickandmortyapp.base.BaseRepository +import com.example.rickandmortyapp.data.implementation.pagingsources.CharactersPagingSource +import com.example.rickandmortyapp.data.network.apiservices.CharactersApiService +import com.example.rickandmortyapp.domain.repositories.CharactersRepository +import javax.inject.Inject + +class CharactersRepositoryImpl @Inject constructor( + private val service: CharactersApiService +) : BaseRepository(), CharactersRepository { + + override fun getCharacters(name: String) = + doPagingRequest(CharactersPagingSource(service, name)) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/implementation/EpisodesRepositoryImpl.kt b/app/src/main/java/com/example/rickandmortyapp/data/implementation/EpisodesRepositoryImpl.kt new file mode 100644 index 0000000..3339e22 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/implementation/EpisodesRepositoryImpl.kt @@ -0,0 +1,14 @@ +package com.example.rickandmortyapp.data.implementation + +import com.example.rickandmortyapp.base.BaseRepository +import com.example.rickandmortyapp.data.implementation.pagingsources.EpisodesPagingSource +import com.example.rickandmortyapp.data.network.apiservices.EpisodesApiService +import com.example.rickandmortyapp.domain.repositories.EpisodesRepository +import javax.inject.Inject + +class EpisodesRepositoryImpl @Inject constructor( + private val service: EpisodesApiService +) : BaseRepository(), EpisodesRepository { + + override fun getEpisodes(name: String) = doPagingRequest(EpisodesPagingSource(service, name)) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/implementation/LocationsRepositoryImpl.kt b/app/src/main/java/com/example/rickandmortyapp/data/implementation/LocationsRepositoryImpl.kt new file mode 100644 index 0000000..e15061d --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/implementation/LocationsRepositoryImpl.kt @@ -0,0 +1,14 @@ +package com.example.rickandmortyapp.data.implementation + +import com.example.rickandmortyapp.base.BaseRepository +import com.example.rickandmortyapp.data.implementation.pagingsources.LocationsPagingSource +import com.example.rickandmortyapp.data.network.apiservices.LocationsApiService +import com.example.rickandmortyapp.domain.repositories.LocationsRepository +import javax.inject.Inject + +class LocationsRepositoryImpl @Inject constructor( + private val service: LocationsApiService +) : BaseRepository(), LocationsRepository { + + override fun getLocations(name: String) = doPagingRequest(LocationsPagingSource(service, name)) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/CharactersPagingSource.kt b/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/CharactersPagingSource.kt new file mode 100644 index 0000000..9ac32b6 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/CharactersPagingSource.kt @@ -0,0 +1,15 @@ +package com.example.rickandmortyapp.data.implementation.pagingsources + +import com.example.rickandmortyapp.base.BasePagingSource +import com.example.rickandmortyapp.data.network.apiservices.CharactersApiService +import com.example.rickandmortyapp.data.network.dtos.models.RickAndMortyDto +import com.example.rickandmortyapp.data.network.dtos.models.toCharacters +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class CharactersPagingSource( + private val service: CharactersApiService, + private val name: String +) : BasePagingSource( + { service.getCharacters(it, name) }, + { data -> data.map { it.toCharacters() } } +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/EpisodesPagingSource.kt b/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/EpisodesPagingSource.kt new file mode 100644 index 0000000..e9194b3 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/EpisodesPagingSource.kt @@ -0,0 +1,16 @@ +package com.example.rickandmortyapp.data.implementation.pagingsources + +import com.example.rickandmortyapp.base.BasePagingSource +import com.example.rickandmortyapp.data.network.apiservices.EpisodesApiService +import com.example.rickandmortyapp.data.network.dtos.models.RickAndMortyDto +import com.example.rickandmortyapp.data.network.dtos.models.toEpisodes +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class EpisodesPagingSource( + private val service: EpisodesApiService, + private val name: String +) : + BasePagingSource( + { service.getEpisodes(it, name) }, + { data -> data.map { it.toEpisodes() } } + ) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/LocationsPagingSource.kt b/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/LocationsPagingSource.kt new file mode 100644 index 0000000..f77d7ee --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/implementation/pagingsources/LocationsPagingSource.kt @@ -0,0 +1,16 @@ +package com.example.rickandmortyapp.data.implementation.pagingsources + +import com.example.rickandmortyapp.base.BasePagingSource +import com.example.rickandmortyapp.data.network.apiservices.LocationsApiService +import com.example.rickandmortyapp.data.network.dtos.models.RickAndMortyDto +import com.example.rickandmortyapp.data.network.dtos.models.toLocations +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class LocationsPagingSource( + private val service: LocationsApiService, + private val name: String +) : + BasePagingSource( + { service.getLocations(it, name) }, + { data -> data.map { it.toLocations() } } + ) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/CharactersApiService.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/CharactersApiService.kt new file mode 100644 index 0000000..7bfbcb9 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/CharactersApiService.kt @@ -0,0 +1,15 @@ +package com.example.rickandmortyapp.data.network.apiservices + +import com.example.rickandmortyapp.data.network.dtos.models.ApiResponse +import com.example.rickandmortyapp.data.network.dtos.models.RickAndMortyDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface CharactersApiService { + + @GET("api/character") + suspend fun getCharacters( + @Query("page") page: Int, + @Query("name") name: String + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/EpisodesApiService.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/EpisodesApiService.kt new file mode 100644 index 0000000..295547f --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/EpisodesApiService.kt @@ -0,0 +1,15 @@ +package com.example.rickandmortyapp.data.network.apiservices + +import com.example.rickandmortyapp.data.network.dtos.models.ApiResponse +import com.example.rickandmortyapp.data.network.dtos.models.RickAndMortyDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface EpisodesApiService { + + @GET("api/episode") + suspend fun getEpisodes( + @Query("page") page: Int, + @Query("name") name: String + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/LocationsApiService.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/LocationsApiService.kt new file mode 100644 index 0000000..72855d3 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/apiservices/LocationsApiService.kt @@ -0,0 +1,15 @@ +package com.example.rickandmortyapp.data.network.apiservices + +import com.example.rickandmortyapp.data.network.dtos.models.ApiResponse +import com.example.rickandmortyapp.data.network.dtos.models.RickAndMortyDto +import retrofit2.http.GET +import retrofit2.http.Query + +interface LocationsApiService { + + @GET("api/location") + suspend fun getLocations( + @Query("page") page: Int, + @Query("name") name: String + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/ApiResponse.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/ApiResponse.kt new file mode 100644 index 0000000..2a93b6a --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/ApiResponse.kt @@ -0,0 +1,12 @@ +package com.example.rickandmortyapp.data.network.dtos.models + +import com.google.gson.annotations.SerializedName + +data class ApiResponse( + + @SerializedName("results") + val results: ArrayList, + + @SerializedName("info") + val info: Info +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/Info.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/Info.kt new file mode 100644 index 0000000..1ffaef4 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/Info.kt @@ -0,0 +1,18 @@ +package com.example.rickandmortyapp.data.network.dtos.models + +import com.google.gson.annotations.SerializedName + +data class Info( + + @SerializedName("next") + val next: String?, + + @SerializedName("pages") + val pages: Int?, + + @SerializedName("prev") + val prev: String?, + + @SerializedName("count") + val count: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/Mapper.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/Mapper.kt new file mode 100644 index 0000000..d4e7ef7 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/Mapper.kt @@ -0,0 +1,95 @@ +package com.example.rickandmortyapp.data.network.dtos.models + +import com.example.rickandmortyapp.data.network.dtos.models.character.CharactersModelDto +import com.example.rickandmortyapp.data.network.dtos.models.character.LocationDto +import com.example.rickandmortyapp.data.network.dtos.models.character.OriginDto +import com.example.rickandmortyapp.data.network.dtos.models.episode.EpisodesModelDto +import com.example.rickandmortyapp.data.network.dtos.models.location.LocationsModelDto +import com.example.rickandmortyapp.domain.models.RickAndMorty +import com.example.rickandmortyapp.domain.models.character.CharactersModel +import com.example.rickandmortyapp.domain.models.character.Location +import com.example.rickandmortyapp.domain.models.character.Origin +import com.example.rickandmortyapp.domain.models.episode.EpisodesModel +import com.example.rickandmortyapp.domain.models.location.LocationsModel + +fun RickAndMortyDto.CharactersItem.toCharacters(): RickAndMorty.CharactersItem = + RickAndMorty.CharactersItem( + image, + gender, + species, + created, + origin.toOrigin(), + name, + location.toLocation(), + episode, + id, + type, + url, + status + ) + +fun CharactersModelDto.toCharactersModel(): CharactersModel = + CharactersModel( + image, + gender, + species, + created, + origin.toOrigin(), + name, + location.toLocation(), + episode, + id, + type, + url, + status + ) + +fun OriginDto.toOrigin(): Origin = + Origin(name, url) + +fun LocationDto.toLocation(): Location = + Location(name, url) + +fun RickAndMortyDto.LocationsItem.toLocations(): RickAndMorty.LocationsItem = + RickAndMorty.LocationsItem( + created, + name, + residents, + id, + type, + dimension, + url + ) + +fun LocationsModelDto.toLocationsModel(): LocationsModel = + LocationsModel( + created, + name, + residents, + id, + type, + dimension, + url + ) + +fun RickAndMortyDto.EpisodesItem.toEpisodes(): RickAndMorty.EpisodesItem = + RickAndMorty.EpisodesItem( + airDate, + characters, + created, + episode, + name, + id, + url + ) + +fun EpisodesModelDto.toEpisodesModel(): EpisodesModel = + EpisodesModel( + airDate, + characters, + created, + episode, + name, + id, + url + ) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/RickAndMortyDto.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/RickAndMortyDto.kt new file mode 100644 index 0000000..b81093f --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/RickAndMortyDto.kt @@ -0,0 +1,95 @@ +package com.example.rickandmortyapp.data.network.dtos.models + +import com.example.rickandmortyapp.data.network.dtos.models.character.LocationDto +import com.example.rickandmortyapp.data.network.dtos.models.character.OriginDto +import com.google.gson.annotations.SerializedName + +sealed class RickAndMortyDto { + + data class CharactersItem( + + @SerializedName("image") + val image: String?, + + @SerializedName("gender") + val gender: String?, + + @SerializedName("species") + val species: String?, + + @SerializedName("created") + val created: String?, + + @SerializedName("origin") + val origin: OriginDto, + + @SerializedName("name") + val name: String?, + + @SerializedName("location") + val location: LocationDto, + + @SerializedName("episode") + val episode: List?, + + @SerializedName("id") + val id: Int?, + + @SerializedName("type") + val type: String?, + + @SerializedName("url") + val url: String?, + + @SerializedName("status") + val status: String? + ) : RickAndMortyDto() + + data class EpisodesItem( + + @SerializedName("air_date") + val airDate: String?, + + @SerializedName("characters") + val characters: List?, + + @SerializedName("created") + val created: String?, + + @SerializedName("episode") + val episode: String?, + + @SerializedName("name") + val name: String?, + + @SerializedName("id") + val id: Int?, + + @SerializedName("url") + val url: String? + ) : RickAndMortyDto() + + data class LocationsItem( + + @SerializedName("created") + val created: String?, + + @SerializedName("name") + val name: String?, + + @SerializedName("residents") + val residents: List?, + + @SerializedName("id") + val id: Int?, + + @SerializedName("type") + val type: String?, + + @SerializedName("dimension") + val dimension: String?, + + @SerializedName("url") + val url: String? + ) : RickAndMortyDto() +} diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/CharactersModelDto.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/CharactersModelDto.kt new file mode 100644 index 0000000..ff09ac9 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/CharactersModelDto.kt @@ -0,0 +1,43 @@ +package com.example.rickandmortyapp.data.network.dtos.models.character + +import com.example.rickandmortyapp.base.IBaseDiffModel +import com.google.gson.annotations.SerializedName + +data class CharactersModelDto( + + @SerializedName("image") + val image: String?, + + @SerializedName("gender") + val gender: String?, + + @SerializedName("species") + val species: String?, + + @SerializedName("created") + val created: String?, + + @SerializedName("origin") + val origin: OriginDto, + + @SerializedName("name") + val name: String?, + + @SerializedName("location") + val location: LocationDto, + + @SerializedName("episode") + val episode: List?, + + @SerializedName("id") + override val id: Int?, + + @SerializedName("type") + val type: String?, + + @SerializedName("url") + val url: String?, + + @SerializedName("status") + val status: String? +) : IBaseDiffModel diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/LocationDto.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/LocationDto.kt new file mode 100644 index 0000000..2cf7b3d --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/LocationDto.kt @@ -0,0 +1,6 @@ +package com.example.rickandmortyapp.data.network.dtos.models.character + +data class LocationDto( + val name: String?, + val url: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/OriginDto.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/OriginDto.kt new file mode 100644 index 0000000..8e38a89 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/character/OriginDto.kt @@ -0,0 +1,6 @@ +package com.example.rickandmortyapp.data.network.dtos.models.character + +data class OriginDto( + val name: String?, + val url: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/episode/EpisodesModelDto.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/episode/EpisodesModelDto.kt new file mode 100644 index 0000000..04402fc --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/episode/EpisodesModelDto.kt @@ -0,0 +1,28 @@ +package com.example.rickandmortyapp.data.network.dtos.models.episode + +import com.example.rickandmortyapp.base.IBaseDiffModel +import com.google.gson.annotations.SerializedName + +data class EpisodesModelDto( + + @SerializedName("air_date") + val airDate: String?, + + @SerializedName("characters") + val characters: List?, + + @SerializedName("created") + val created: String?, + + @SerializedName("episode") + val episode: String?, + + @SerializedName("name") + val name: String?, + + @SerializedName("id") + override val id: Int?, + + @SerializedName("url") + val url: String? +) : IBaseDiffModel \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/location/LocationsModelDto.kt b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/location/LocationsModelDto.kt new file mode 100644 index 0000000..ee2005e --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/data/network/dtos/models/location/LocationsModelDto.kt @@ -0,0 +1,28 @@ +package com.example.rickandmortyapp.data.network.dtos.models.location + +import com.example.rickandmortyapp.base.IBaseDiffModel +import com.google.gson.annotations.SerializedName + +data class LocationsModelDto( + + @SerializedName("created") + val created: String?, + + @SerializedName("name") + val name: String?, + + @SerializedName("residents") + val residents: List?, + + @SerializedName("id") + override val id: Int?, + + @SerializedName("type") + val type: String?, + + @SerializedName("dimension") + val dimension: String?, + + @SerializedName("url") + val url: String? +) : IBaseDiffModel \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/di/NetworkModule.kt b/app/src/main/java/com/example/rickandmortyapp/di/NetworkModule.kt new file mode 100644 index 0000000..953005a --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/di/NetworkModule.kt @@ -0,0 +1,58 @@ +package com.example.rickandmortyapp.di + +import com.example.rickandmortyapp.common.constants.Constants +import com.example.rickandmortyapp.data.network.apiservices.CharactersApiService +import com.example.rickandmortyapp.data.network.apiservices.EpisodesApiService +import com.example.rickandmortyapp.data.network.apiservices.LocationsApiService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Singleton + @Provides + fun createOkHttpClient() = + OkHttpClient().newBuilder() + .addInterceptor( + HttpLoggingInterceptor().setLevel( + HttpLoggingInterceptor.Level.BODY + ) + ) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + @Singleton + @Provides + fun createRetrofitClient(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() + .baseUrl(Constants.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build() + + @Singleton + @Provides + fun provideCharactersApiService(retrofit: Retrofit): CharactersApiService = + retrofit.create(CharactersApiService::class.java) + + @Singleton + @Provides + fun provideLocationsApiService(retrofit: Retrofit): LocationsApiService = + retrofit.create(LocationsApiService::class.java) + + @Singleton + @Provides + fun provideEpisodesApiService(retrofit: Retrofit): EpisodesApiService = + retrofit.create(EpisodesApiService::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/di/RepositoriesModule.kt b/app/src/main/java/com/example/rickandmortyapp/di/RepositoriesModule.kt new file mode 100644 index 0000000..599287d --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/di/RepositoriesModule.kt @@ -0,0 +1,32 @@ +package com.example.rickandmortyapp.di + +import com.example.rickandmortyapp.data.implementation.CharactersRepositoryImpl +import com.example.rickandmortyapp.data.implementation.EpisodesRepositoryImpl +import com.example.rickandmortyapp.data.implementation.LocationsRepositoryImpl +import com.example.rickandmortyapp.data.network.apiservices.CharactersApiService +import com.example.rickandmortyapp.data.network.apiservices.EpisodesApiService +import com.example.rickandmortyapp.data.network.apiservices.LocationsApiService +import com.example.rickandmortyapp.domain.repositories.CharactersRepository +import com.example.rickandmortyapp.domain.repositories.EpisodesRepository +import com.example.rickandmortyapp.domain.repositories.LocationsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object RepositoriesModule { + + @Provides + fun provideCharacterRepository(service: CharactersApiService): CharactersRepository = + CharactersRepositoryImpl(service) + + @Provides + fun provideLocationsRepository(service: LocationsApiService): LocationsRepository = + LocationsRepositoryImpl(service) + + @Provides + fun provideEpisodesRepository(service: EpisodesApiService): EpisodesRepository = + EpisodesRepositoryImpl(service) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/PagingData.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/PagingData.kt new file mode 100644 index 0000000..515245a --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/PagingData.kt @@ -0,0 +1,3 @@ +package com.example.rickandmortyapp.domain.models + +typealias PagingList = Any \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/RickAndMorty.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/RickAndMorty.kt new file mode 100644 index 0000000..e392f80 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/RickAndMorty.kt @@ -0,0 +1,42 @@ +package com.example.rickandmortyapp.domain.models + +import com.example.rickandmortyapp.domain.models.character.Location +import com.example.rickandmortyapp.domain.models.character.Origin + +sealed class RickAndMorty { + + data class CharactersItem( + val image: String?, + val gender: String?, + val species: String?, + val created: String?, + val origin: Origin, + val name: String?, + val location: Location, + val episode: List?, + val id: Int?, + val type: String?, + val url: String?, + val status: String? + ) : RickAndMorty() + + data class EpisodesItem( + val airDate: String?, + val characters: List?, + val created: String?, + val episode: String?, + val name: String?, + val id: Int?, + val url: String? + ) : RickAndMorty() + + data class LocationsItem( + val created: String?, + val name: String?, + val residents: List?, + val id: Int?, + val type: String?, + val dimension: String?, + val url: String? + ) : RickAndMorty() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/character/CharactersModel.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/character/CharactersModel.kt new file mode 100644 index 0000000..839950e --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/character/CharactersModel.kt @@ -0,0 +1,18 @@ +package com.example.rickandmortyapp.domain.models.character + +import com.example.rickandmortyapp.base.IBaseDiffModel + +data class CharactersModel( + val image: String?, + val gender: String?, + val species: String?, + val created: String?, + val origin: Origin, + val name: String?, + val location: Location, + val episode: List?, + override val id: Int?, + val type: String?, + val url: String?, + val status: String? +) : IBaseDiffModel \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/character/Location.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/character/Location.kt new file mode 100644 index 0000000..431e3b0 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/character/Location.kt @@ -0,0 +1,6 @@ +package com.example.rickandmortyapp.domain.models.character + +data class Location( + val name: String?, + val url: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/character/Origin.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/character/Origin.kt new file mode 100644 index 0000000..8debbc8 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/character/Origin.kt @@ -0,0 +1,6 @@ +package com.example.rickandmortyapp.domain.models.character + +data class Origin( + val name: String?, + val url: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/episode/EpisodesModel.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/episode/EpisodesModel.kt new file mode 100644 index 0000000..e272c51 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/episode/EpisodesModel.kt @@ -0,0 +1,13 @@ +package com.example.rickandmortyapp.domain.models.episode + +import com.example.rickandmortyapp.base.IBaseDiffModel + +data class EpisodesModel( + val airDate: String?, + val characters: List?, + val created: String?, + val episode: String?, + val name: String?, + override val id: Int?, + val url: String? +) : IBaseDiffModel \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/models/location/LocationsModel.kt b/app/src/main/java/com/example/rickandmortyapp/domain/models/location/LocationsModel.kt new file mode 100644 index 0000000..5ddacb8 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/models/location/LocationsModel.kt @@ -0,0 +1,13 @@ +package com.example.rickandmortyapp.domain.models.location + +import com.example.rickandmortyapp.base.IBaseDiffModel + +data class LocationsModel( + val created: String?, + val name: String?, + val residents: List?, + override val id: Int?, + val type: String?, + val dimension: String?, + val url: String? +) : IBaseDiffModel \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/repositories/CharactersRepository.kt b/app/src/main/java/com/example/rickandmortyapp/domain/repositories/CharactersRepository.kt new file mode 100644 index 0000000..8ab5ab5 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/repositories/CharactersRepository.kt @@ -0,0 +1,10 @@ +package com.example.rickandmortyapp.domain.repositories + +import com.example.rickandmortyapp.domain.models.PagingList +import com.example.rickandmortyapp.domain.models.RickAndMorty +import kotlinx.coroutines.flow.Flow + +interface CharactersRepository { + + fun getCharacters(name: String): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/repositories/EpisodesRepository.kt b/app/src/main/java/com/example/rickandmortyapp/domain/repositories/EpisodesRepository.kt new file mode 100644 index 0000000..1e0412c --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/repositories/EpisodesRepository.kt @@ -0,0 +1,10 @@ +package com.example.rickandmortyapp.domain.repositories + +import com.example.rickandmortyapp.domain.models.PagingList +import com.example.rickandmortyapp.domain.models.RickAndMorty +import kotlinx.coroutines.flow.Flow + +interface EpisodesRepository { + + fun getEpisodes(name: String): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/repositories/LocationsRepository.kt b/app/src/main/java/com/example/rickandmortyapp/domain/repositories/LocationsRepository.kt new file mode 100644 index 0000000..497656f --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/repositories/LocationsRepository.kt @@ -0,0 +1,10 @@ +package com.example.rickandmortyapp.domain.repositories + +import com.example.rickandmortyapp.domain.models.PagingList +import com.example.rickandmortyapp.domain.models.RickAndMorty +import kotlinx.coroutines.flow.Flow + +interface LocationsRepository { + + fun getLocations(name: String): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetCharactersUseCase.kt b/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetCharactersUseCase.kt new file mode 100644 index 0000000..0bc05a9 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetCharactersUseCase.kt @@ -0,0 +1,11 @@ +package com.example.rickandmortyapp.domain.usecases + +import com.example.rickandmortyapp.domain.repositories.CharactersRepository +import javax.inject.Inject + +class GetCharactersUseCase @Inject constructor( + private val repository: CharactersRepository +) { + + operator fun invoke(name: String) = repository.getCharacters(name) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetEpisodesUseCase.kt b/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetEpisodesUseCase.kt new file mode 100644 index 0000000..f1c7e37 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetEpisodesUseCase.kt @@ -0,0 +1,11 @@ +package com.example.rickandmortyapp.domain.usecases + +import com.example.rickandmortyapp.domain.repositories.EpisodesRepository +import javax.inject.Inject + +class GetEpisodesUseCase @Inject constructor( + private val repository: EpisodesRepository +) { + + operator fun invoke(name: String) = repository.getEpisodes(name) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetLocationsUseCase.kt b/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetLocationsUseCase.kt new file mode 100644 index 0000000..adac942 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/domain/usecases/GetLocationsUseCase.kt @@ -0,0 +1,11 @@ +package com.example.rickandmortyapp.domain.usecases + +import com.example.rickandmortyapp.domain.repositories.LocationsRepository +import javax.inject.Inject + +class GetLocationsUseCase @Inject constructor( + private val repository: LocationsRepository +) { + + operator fun invoke(name: String) = repository.getLocations(name) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/state/UIState.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/state/UIState.kt new file mode 100644 index 0000000..7874955 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/state/UIState.kt @@ -0,0 +1,7 @@ +package com.example.rickandmortyapp.presentation.state + +sealed class UIState { + class Loading : UIState() + class Error(val error: String) : UIState() + class Success(val data: T) : UIState() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/activity/MainActivity.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/activity/MainActivity.kt new file mode 100644 index 0000000..f1b03b6 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/activity/MainActivity.kt @@ -0,0 +1,68 @@ +package com.example.rickandmortyapp.presentation.ui.activity + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.NavigationUI +import androidx.navigation.ui.NavigationUI.setupWithNavController +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : AppCompatActivity(R.layout.activity_main) { + + private lateinit var navController: NavController + private val binding by viewBinding(ActivityMainBinding::bind) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupNavigation() + setupListeners() +// supportActionBar?.setHomeAsUpIndicator(R.drawable.rickandmortytitle) +// supportActionBar?.setHomeButtonEnabled(true) +// supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + private fun setupListeners() { + onDestinationChangedListener() + } + + private fun setupNavigation() { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + navController = navHostFragment.navController + + val appBarConfiguration = AppBarConfiguration.Builder( + R.id.charactersFragment, + R.id.locationsFragment, + R.id.episodesFragment, + R.id.searchFragment + ).build() + + setupWithNavController(binding.bottomNavigation, navController) + NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration) + } + + private fun onDestinationChangedListener() { + navController.addOnDestinationChangedListener { _, destination, _ -> + when (destination.id) { + R.id.charactersFragment + -> { + } + R.id.locationsFragment + -> { + } + R.id.episodesFragment -> { + } + R.id.searchFragment -> { + } + else -> { + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/CharactersAdapter.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/CharactersAdapter.kt new file mode 100644 index 0000000..0047dc6 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/CharactersAdapter.kt @@ -0,0 +1,85 @@ +package com.example.rickandmortyapp.presentation.ui.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.viewbinding.ViewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.base.BaseRecyclerViewHolder +import com.example.rickandmortyapp.common.extensions.loadImage +import com.example.rickandmortyapp.common.extensions.setOnSingleClickListener +import com.example.rickandmortyapp.databinding.ItemCharactersBinding +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class CharactersAdapter(val onClick: (id: Int?) -> Unit) : + PagingDataAdapter>( + CharacterDiffUtil() + ) { + + class CharacterDiffUtil : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: RickAndMorty.CharactersItem, + newItem: RickAndMorty.CharactersItem + ): Boolean { + return oldItem.id == newItem.id + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame( + oldItem: RickAndMorty.CharactersItem, + newItem: RickAndMorty.CharactersItem + ): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BaseRecyclerViewHolder { + return CharactersViewHolder( + ItemCharactersBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder( + holder: BaseRecyclerViewHolder, + position: Int + ) { + getItem(position)?.let { holder.onBind(it) } + } + + inner class CharactersViewHolder( + binding: ItemCharactersBinding + ) : BaseRecyclerViewHolder(binding) { + + override fun onBind(item: RickAndMorty.CharactersItem) = with(binding) { + itemName.text = item.name + itemImage.loadImage(item.image) + itemStatus.text = item.status + itemSpecies.text = item.species + itemLocation.text = item.location.name + itemEpisode.text = item.origin.name + + when (item.status) { + "Alive" -> statusDot.setImageResource(R.drawable.ic_dot) + + "Dead" -> statusDot.setImageResource(R.drawable.ic_dot_red) + + "unknown" -> statusDot.setImageResource(R.drawable.ic_dot_gray) + } + itemView.setOnSingleClickListener { + getItem(absoluteAdapterPosition)?.apply { + onClick(this.id) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/EpisodesAdapter.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/EpisodesAdapter.kt new file mode 100644 index 0000000..0a67fa6 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/EpisodesAdapter.kt @@ -0,0 +1,70 @@ +package com.example.rickandmortyapp.presentation.ui.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.viewbinding.ViewBinding +import com.example.rickandmortyapp.base.BaseRecyclerViewHolder +import com.example.rickandmortyapp.common.extensions.toFormatDate +import com.example.rickandmortyapp.databinding.ItemEpisodesBinding +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class EpisodesAdapter : + PagingDataAdapter>( + EpisodeDiffUtil() + ) { + + class EpisodeDiffUtil : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: RickAndMorty.EpisodesItem, + newItem: RickAndMorty.EpisodesItem + ): Boolean { + return oldItem.id == newItem.id + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame( + oldItem: RickAndMorty.EpisodesItem, + newItem: RickAndMorty.EpisodesItem + ): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BaseRecyclerViewHolder { + return EpisodesViewHolder( + ItemEpisodesBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder( + holder: BaseRecyclerViewHolder, + position: Int + ) { + getItem(position)?.let { holder.onBind(it) } + } + + inner class EpisodesViewHolder( + binding: ItemEpisodesBinding + ) : BaseRecyclerViewHolder(binding) { + + override fun onBind(item: RickAndMorty.EpisodesItem) = with(binding) { + itemName.text = item.name + itemEpisode.text = item.episode + ?.replace("S", "Season ") + ?.replace("E", " Episode ") + itemCreated.text = toFormatDate(item.created) + itemAirDate.text = item.airDate + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/LocationsAdapter.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/LocationsAdapter.kt new file mode 100644 index 0000000..2a9ac7b --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/LocationsAdapter.kt @@ -0,0 +1,70 @@ +package com.example.rickandmortyapp.presentation.ui.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.viewbinding.ViewBinding +import com.example.rickandmortyapp.base.BaseRecyclerViewHolder +import com.example.rickandmortyapp.common.extensions.toFormatDate +import com.example.rickandmortyapp.databinding.ItemLocationsBinding +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class LocationsAdapter : + PagingDataAdapter>( + LocationDiffUtil() + ) { + + class LocationDiffUtil : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: RickAndMorty.LocationsItem, + newItem: RickAndMorty.LocationsItem + ): Boolean { + return oldItem.id == newItem.id + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame( + oldItem: RickAndMorty.LocationsItem, + newItem: RickAndMorty.LocationsItem + ): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BaseRecyclerViewHolder { + return LocationsViewHolder( + ItemLocationsBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder( + holder: BaseRecyclerViewHolder, + position: Int + ) { + getItem(position)?.let { holder.onBind(it) } + } + + inner class LocationsViewHolder( + binding: ItemLocationsBinding + ) : BaseRecyclerViewHolder(binding) { + + override fun onBind(item: RickAndMorty.LocationsItem) = with(binding) { + itemName.text = item.name + itemType.text = item.type + imagePlanet.isVisible = item.type == "Planet" + itemCreated.text = toFormatDate(item.created) + itemDimension.text = item.dimension + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/SearchAdapter.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/SearchAdapter.kt new file mode 100644 index 0000000..8cde6ad --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/SearchAdapter.kt @@ -0,0 +1,133 @@ +package com.example.rickandmortyapp.presentation.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.viewbinding.ViewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.databinding.ItemCharactersBinding +import com.example.rickandmortyapp.databinding.ItemEpisodesBinding +import com.example.rickandmortyapp.databinding.ItemLocationsBinding +import com.example.rickandmortyapp.domain.models.RickAndMorty + +class SearchAdapter : + PagingDataAdapter>( + SearchDiffUtil() + ) { + + class SearchDiffUtil : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: RickAndMorty, newItem: RickAndMorty): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: RickAndMorty, newItem: RickAndMorty): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SearchRecyclerViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + R.layout.item_characters -> SearchRecyclerViewHolder.CharactersViewHolder( + ItemCharactersBinding.inflate( + inflater, + parent, + false + ) + ) + R.layout.item_locations -> SearchRecyclerViewHolder.LocationsViewHolder( + ItemLocationsBinding.inflate( + inflater, + parent, + false + ) + ) + R.layout.item_episodes -> SearchRecyclerViewHolder.EpisodesViewHolder( + ItemEpisodesBinding.inflate( + inflater, + parent, + false + ) + ) + else -> throw IllegalAccessException("Invalid viewType provided") + } + } + + override fun onBindViewHolder( + holder: SearchRecyclerViewHolder, + position: Int + ) { + when (holder) { + is SearchRecyclerViewHolder.CharactersViewHolder -> + holder.onBind(getItem(position) as RickAndMorty.CharactersItem) + is SearchRecyclerViewHolder.LocationsViewHolder -> + holder.onBind(getItem(position) as RickAndMorty.LocationsItem) + is SearchRecyclerViewHolder.EpisodesViewHolder -> + holder.onBind(getItem(position) as RickAndMorty.EpisodesItem) + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is RickAndMorty.CharactersItem -> R.layout.item_characters + is RickAndMorty.LocationsItem -> R.layout.item_locations + is RickAndMorty.EpisodesItem -> R.layout.item_episodes + null -> throw IllegalStateException("Unknown view") + } + } +} + +//inner class CharactersViewHolder( +// binding: ItemCharactersBinding +//) : BaseRecyclerViewHolder(binding) { +// +// override fun onBind(item: RickAndMorty.CharactersItem) = with(binding) { +// itemName.text = item.characters.name +// itemImage.loadImage(item.characters.image) +// itemStatus.text = item.characters.status +// itemSpecies.text = item.characters.species +// itemLocation.text = item.characters.location.name +// itemEpisode.text = item.characters.origin.name +// +// when (item.characters.status) { +// +// "Alive" -> statusDot.setImageResource(R.drawable.ic_dot) +// +// "Dead" -> statusDot.setImageResource(R.drawable.ic_dot_red) +// +// "unknown" -> statusDot.setImageResource(R.drawable.ic_dot_gray) +// } +// } +//} +// +//inner class LocationsViewHolder( +// binding: ItemLocationsBinding +//) : BaseRecyclerViewHolder(binding) { +// +// override fun onBind(item: RickAndMorty.LocationsItem) = with(binding) { +// itemName.text = item.locations.name +// itemType.text = item.locations.type +// imagePlanet.isVisible = item.locations.type == "Planet" +// itemCreated.text = toFormatDate(item.locations.created) +// itemDimension.text = item.locations.dimension +// } +//} +// +//inner class EpisodesViewHolder( +// binding: ItemEpisodesBinding +//) : BaseRecyclerViewHolder(binding) { +// +// override fun onBind(item: RickAndMorty.EpisodesItem) = with(binding) { +// itemName.text = item.episodes.name +// itemEpisode.text = item.episodes.episode +// ?.replace("S", "Season ") +// ?.replace("E", " Episode ") +// itemCreated.text = toFormatDate(item.episodes.created) +// itemAirDate.text = item.episodes.airDate +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/SearchRecyclerViewHolder.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/SearchRecyclerViewHolder.kt new file mode 100644 index 0000000..bc6189b --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/SearchRecyclerViewHolder.kt @@ -0,0 +1,66 @@ +package com.example.rickandmortyapp.presentation.ui.adapters + +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.common.extensions.loadImage +import com.example.rickandmortyapp.common.extensions.toFormatDate +import com.example.rickandmortyapp.databinding.ItemCharactersBinding +import com.example.rickandmortyapp.databinding.ItemEpisodesBinding +import com.example.rickandmortyapp.databinding.ItemLocationsBinding +import com.example.rickandmortyapp.domain.models.RickAndMorty + +sealed class SearchRecyclerViewHolder(binding: V) : + RecyclerView.ViewHolder(binding.root) { + + class CharactersViewHolder( + private val binding: ItemCharactersBinding + ) : SearchRecyclerViewHolder(binding) { + + fun onBind(item: RickAndMorty.CharactersItem) = with(binding) { + itemName.text = item.name + itemImage.loadImage(item.image) + itemStatus.text = item.status + itemSpecies.text = item.species + itemLocation.text = item.location.name + itemEpisode.text = item.origin.name + + when (item.status) { + + "Alive" -> statusDot.setImageResource(R.drawable.ic_dot) + + "Dead" -> statusDot.setImageResource(R.drawable.ic_dot_red) + + "unknown" -> statusDot.setImageResource(R.drawable.ic_dot_gray) + } + } + } + + class LocationsViewHolder( + private val binding: ItemLocationsBinding + ) : SearchRecyclerViewHolder(binding) { + + fun onBind(item: RickAndMorty.LocationsItem) = with(binding) { + itemName.text = item.name + itemType.text = item.type + imagePlanet.isVisible = item.type == "Planet" + itemCreated.text = toFormatDate(item.created) + itemDimension.text = item.dimension + } + } + + class EpisodesViewHolder( + private val binding: ItemEpisodesBinding + ) : SearchRecyclerViewHolder(binding) { + + fun onBind(item: RickAndMorty.EpisodesItem) = with(binding) { + itemName.text = item.name + itemEpisode.text = item.episode + ?.replace("S", "Season ") + ?.replace("E", " Episode ") + itemCreated.text = toFormatDate(item.created) + itemAirDate.text = item.airDate + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/paging/LoadStateAdapter.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/paging/LoadStateAdapter.kt new file mode 100644 index 0000000..4b28214 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/paging/LoadStateAdapter.kt @@ -0,0 +1,16 @@ +package com.example.rickandmortyapp.presentation.ui.adapters.paging + +import android.view.ViewGroup +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter + +class LoadStateAdapter(private val retry: () -> Unit) : LoadStateAdapter() { + + override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) { + holder.onBind(loadState) + } + + override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder { + return LoadStateViewHolder.create(parent, retry) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/paging/LoadStateViewHolder.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/paging/LoadStateViewHolder.kt new file mode 100644 index 0000000..179e0f8 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/adapters/paging/LoadStateViewHolder.kt @@ -0,0 +1,40 @@ +package com.example.rickandmortyapp.presentation.ui.adapters.paging + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.paging.LoadState +import androidx.recyclerview.widget.RecyclerView +import com.example.rickandmortyapp.common.extensions.setOnSingleClickListener +import com.example.rickandmortyapp.databinding.ItemLoadFooterBinding + +class LoadStateViewHolder( + private val binding: ItemLoadFooterBinding, + retry: () -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.btnRetry.setOnSingleClickListener { + retry.invoke() + } + } + + fun onBind(loadState: LoadState) { + binding.progressBar.isVisible = loadState is LoadState.Loading + binding.btnRetry.isVisible = loadState is LoadState.Error + binding.errorMsg.isVisible = loadState is LoadState.Error + } + + companion object { + fun create(parent: ViewGroup, retry: () -> Unit): LoadStateViewHolder { + return LoadStateViewHolder( + ItemLoadFooterBinding.inflate( + LayoutInflater + .from(parent.context), parent, + false + ), retry + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/characters/CharactersFragment.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/characters/CharactersFragment.kt new file mode 100644 index 0000000..c48864c --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/characters/CharactersFragment.kt @@ -0,0 +1,92 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.characters + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.base.BaseFragment +import com.example.rickandmortyapp.common.extensions.hideKeyboard +import com.example.rickandmortyapp.common.extensions.setOnActionExpandListener +import com.example.rickandmortyapp.common.extensions.setTools +import com.example.rickandmortyapp.common.extensions.submitSearch +import com.example.rickandmortyapp.databinding.FragmentCharactersBinding +import com.example.rickandmortyapp.presentation.ui.adapters.CharactersAdapter +import com.example.rickandmortyapp.presentation.ui.adapters.paging.LoadStateAdapter +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class CharactersFragment : BaseFragment( + R.layout.fragment_characters +) { + + override val binding by viewBinding(FragmentCharactersBinding::bind) + override val viewModel by viewModels() + private val adapter = CharactersAdapter(this::onClickToDetail) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun initialize() { + setupRecycler() + } + + private fun setupRecycler() = with(binding) { + binding.charactersRecycler.layoutManager = LinearLayoutManager(requireContext()) + binding.charactersRecycler.adapter = adapter.withLoadStateFooter(LoadStateAdapter { + adapter.retry() + }) + } + + override fun setupViews() { + setupProgressBar() + } + + private fun setupProgressBar() = with(binding) { + adapter.addLoadStateListener { + if (view != null) { + charactersLoading.isVisible = it.refresh is LoadState.Loading + container.containerNotFound.isVisible = it.refresh is LoadState.Error + charactersRecycler.isVisible = it.refresh !is LoadState.Error + } + } + } + + override fun setupRequests() { + viewModel.getCharacters("") + } + + override fun setupObserves() { + viewModel.charactersState.observe(viewLifecycleOwner, { + lifecycleScope.launch { + adapter.submitData(it) + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.toolbar_menu, menu) + val searchItem = menu.findItem(R.id.search) + val searchView = searchItem.actionView as SearchView + + searchView.setTools(context) + + searchView.submitSearch { viewModel.getCharacters(it.toString()) } + + searchItem.setOnActionExpandListener(searchView) { hideKeyboard() } + } + + private fun onClickToDetail(id: Int?) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/characters/CharactersViewModel.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/characters/CharactersViewModel.kt new file mode 100644 index 0000000..636bcca --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/characters/CharactersViewModel.kt @@ -0,0 +1,28 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.characters + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import com.example.rickandmortyapp.base.BaseViewModel +import com.example.rickandmortyapp.domain.models.RickAndMorty +import com.example.rickandmortyapp.domain.usecases.GetCharactersUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CharactersViewModel @Inject constructor( + private val getAllCharacters: GetCharactersUseCase +) : BaseViewModel() { + + private val _charactersState = MutableLiveData>() + val charactersState: LiveData> = _charactersState + + fun getCharacters(name: String) = viewModelScope.launch { + getAllCharacters(name).collect { + _charactersState.value = it as PagingData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/episodes/EpisodesFragment.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/episodes/EpisodesFragment.kt new file mode 100644 index 0000000..847b149 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/episodes/EpisodesFragment.kt @@ -0,0 +1,86 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.episodes + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.base.BaseFragment +import com.example.rickandmortyapp.common.extensions.hideKeyboard +import com.example.rickandmortyapp.common.extensions.setOnActionExpandListener +import com.example.rickandmortyapp.common.extensions.setTools +import com.example.rickandmortyapp.common.extensions.submitSearch +import com.example.rickandmortyapp.databinding.FragmentEpisodesBinding +import com.example.rickandmortyapp.presentation.ui.adapters.EpisodesAdapter +import com.example.rickandmortyapp.presentation.ui.adapters.paging.LoadStateAdapter +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class EpisodesFragment : BaseFragment( + R.layout.fragment_episodes +) { + + override val binding by viewBinding(FragmentEpisodesBinding::bind) + override val viewModel by viewModels() + private val adapter: EpisodesAdapter = EpisodesAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun initialize() { + setupRecycler() + } + + private fun setupRecycler() = with(binding) { + episodesRecycler.layoutManager = LinearLayoutManager(requireContext()) + episodesRecycler.adapter = adapter.withLoadStateFooter(LoadStateAdapter { + adapter.retry() + }) + } + + override fun setupViews() { + setupProgressBar() + } + + private fun setupProgressBar() { + adapter.addLoadStateListener { + if (view != null) { + binding.episodesLoading.isVisible = it.refresh is LoadState.Loading + } + } + } + + override fun setupRequests() { + viewModel.getEpisodes("") + } + + override fun setupObserves() { + viewModel.episodesSate.observe(viewLifecycleOwner, { + lifecycleScope.launch { + adapter.submitData(it) + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.toolbar_menu, menu) + val searchItem = menu.findItem(R.id.search) + val searchView = searchItem.actionView as SearchView + + searchView.setTools(context) + + searchView.submitSearch { viewModel.getEpisodes(it.toString()) } + + searchItem.setOnActionExpandListener(searchView) { hideKeyboard() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/episodes/EpisodesViewModel.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/episodes/EpisodesViewModel.kt new file mode 100644 index 0000000..69c96ac --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/episodes/EpisodesViewModel.kt @@ -0,0 +1,28 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.episodes + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import com.example.rickandmortyapp.base.BaseViewModel +import com.example.rickandmortyapp.domain.models.RickAndMorty +import com.example.rickandmortyapp.domain.usecases.GetEpisodesUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class EpisodesViewModel @Inject constructor( + private val getAllEpisodes: GetEpisodesUseCase +) : BaseViewModel() { + + private val _episodesState = MutableLiveData>() + val episodesSate: LiveData> = _episodesState + + fun getEpisodes(name: String) = viewModelScope.launch { + getAllEpisodes(name).collect { + _episodesState.value = it as PagingData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/locations/LocationsFragment.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/locations/LocationsFragment.kt new file mode 100644 index 0000000..3a84771 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/locations/LocationsFragment.kt @@ -0,0 +1,85 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.locations + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.base.BaseFragment +import com.example.rickandmortyapp.common.extensions.hideKeyboard +import com.example.rickandmortyapp.common.extensions.setOnActionExpandListener +import com.example.rickandmortyapp.common.extensions.setTools +import com.example.rickandmortyapp.common.extensions.submitSearch +import com.example.rickandmortyapp.databinding.FragmentLocationsBinding +import com.example.rickandmortyapp.presentation.ui.adapters.LocationsAdapter +import com.example.rickandmortyapp.presentation.ui.adapters.paging.LoadStateAdapter +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class LocationsFragment : + BaseFragment(R.layout.fragment_locations) { + + override val binding by viewBinding(FragmentLocationsBinding::bind) + override val viewModel by viewModels() + private val adapter: LocationsAdapter = LocationsAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun initialize() { + setupRecycler() + } + + private fun setupRecycler() = with(binding) { + locationsRecycler.layoutManager = LinearLayoutManager(requireContext()) + locationsRecycler.adapter = adapter.withLoadStateFooter(LoadStateAdapter { + adapter.retry() + }) + } + + override fun setupViews() { + setupProgressBar() + } + + private fun setupProgressBar() { + adapter.addLoadStateListener { + if (view != null) { + binding.locationsLoading.isVisible = it.refresh is LoadState.Loading + } + } + } + + override fun setupRequests() { + viewModel.getLocations("") + } + + override fun setupObserves() { + viewModel.locationsState.observe(viewLifecycleOwner, { + lifecycleScope.launch { + adapter.submitData(it) + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.toolbar_menu, menu) + val searchItem = menu.findItem(R.id.search) + val searchView = searchItem.actionView as SearchView + + searchView.setTools(context) + + searchView.submitSearch { viewModel.getLocations(it.toString()) } + + searchItem.setOnActionExpandListener(searchView) { hideKeyboard() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/locations/LocationsViewModel.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/locations/LocationsViewModel.kt new file mode 100644 index 0000000..8113f93 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/locations/LocationsViewModel.kt @@ -0,0 +1,29 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.locations + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import com.example.rickandmortyapp.base.BaseViewModel +import com.example.rickandmortyapp.domain.models.RickAndMorty +import com.example.rickandmortyapp.domain.usecases.GetLocationsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class LocationsViewModel @Inject constructor( + private val getAllLocations: GetLocationsUseCase +) : BaseViewModel() { + + private val _locationsState = MutableLiveData>() + val locationsState: LiveData> = _locationsState + + fun getLocations(name: String) = viewModelScope.launch { + getAllLocations(name).collect { + _locationsState.value = it as PagingData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/search/SearchFragment.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/search/SearchFragment.kt new file mode 100644 index 0000000..eecd704 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/search/SearchFragment.kt @@ -0,0 +1,71 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.search + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.widget.SearchView +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.rickandmortyapp.R +import com.example.rickandmortyapp.base.BaseFragment +import com.example.rickandmortyapp.common.extensions.hideKeyboard +import com.example.rickandmortyapp.common.extensions.setOnActionExpandListener +import com.example.rickandmortyapp.common.extensions.setTools +import com.example.rickandmortyapp.common.extensions.submitSearch +import com.example.rickandmortyapp.databinding.FragmentSearchBinding +import com.example.rickandmortyapp.presentation.ui.adapters.SearchAdapter +import com.example.rickandmortyapp.presentation.ui.adapters.paging.LoadStateAdapter +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SearchFragment : BaseFragment( + R.layout.fragment_search +) { + override val binding by viewBinding(FragmentSearchBinding::bind) + override val viewModel by viewModels() + private val adapter: SearchAdapter = SearchAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun initialize() { + setupRecycler() + } + + private fun setupRecycler() = with(binding) { + searchRecycler.layoutManager = LinearLayoutManager(requireContext()) + searchRecycler.adapter = adapter.withLoadStateFooter(LoadStateAdapter { + adapter.retry() + }) + } + + override fun setupRequests() { + viewModel.processAllRequest("") + } + + override fun setupObserves() { + viewModel.allState.observe(viewLifecycleOwner, { + lifecycleScope.launch { + adapter.submitData(it) + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.toolbar_menu, menu) + val searchItem = menu.findItem(R.id.search) + val searchView = searchItem.actionView as SearchView + + searchView.setTools(context) + + searchView.submitSearch { viewModel.processAllRequest(it.toString()) } + + searchItem.setOnActionExpandListener(searchView) { hideKeyboard() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/search/SearchViewModel.kt b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/search/SearchViewModel.kt new file mode 100644 index 0000000..82fb0a1 --- /dev/null +++ b/app/src/main/java/com/example/rickandmortyapp/presentation/ui/fragments/search/SearchViewModel.kt @@ -0,0 +1,55 @@ +package com.example.rickandmortyapp.presentation.ui.fragments.search + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import com.example.rickandmortyapp.base.BaseViewModel +import com.example.rickandmortyapp.domain.models.RickAndMorty +import com.example.rickandmortyapp.domain.usecases.GetCharactersUseCase +import com.example.rickandmortyapp.domain.usecases.GetEpisodesUseCase +import com.example.rickandmortyapp.domain.usecases.GetLocationsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val getAllCharacters: GetCharactersUseCase, + private val getAllLocations: GetLocationsUseCase, + private val getAllEpisodes: GetEpisodesUseCase +) : BaseViewModel() { + + private val _allState = MutableLiveData>() + val allState: LiveData> = _allState + + fun processAllRequest(name: String) = viewModelScope.launch { + val charactersDeferred = + withContext(Dispatchers.IO) { + getAllCharacters(name) + } + val locationsDeferred = + withContext(Dispatchers.IO) { + getAllLocations(name) + } + val episodesDeferred = + withContext(Dispatchers.IO) { + getAllEpisodes(name) + } + + charactersDeferred.collect { + _allState.postValue(it as PagingData) + } + + locationsDeferred.collect { + _allState.postValue(it as PagingData) + } + + episodesDeferred.collect { + _allState.postValue(it as PagingData) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_nav_background.xml b/app/src/main/res/drawable/bottom_nav_background.xml new file mode 100644 index 0000000..386f06d --- /dev/null +++ b/app/src/main/res/drawable/bottom_nav_background.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml new file mode 100644 index 0000000..8e81e0a --- /dev/null +++ b/app/src/main/res/drawable/ic_dot.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_dot_gray.xml b/app/src/main/res/drawable/ic_dot_gray.xml new file mode 100644 index 0000000..0101cd8 --- /dev/null +++ b/app/src/main/res/drawable/ic_dot_gray.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_dot_red.xml b/app/src/main/res/drawable/ic_dot_red.xml new file mode 100644 index 0000000..2ce2623 --- /dev/null +++ b/app/src/main/res/drawable/ic_dot_red.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_earth.xml b/app/src/main/res/drawable/ic_earth.xml new file mode 100644 index 0000000..b3555d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_earth.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_episodes.xml b/app/src/main/res/drawable/ic_episodes.xml new file mode 100644 index 0000000..2674bb3 --- /dev/null +++ b/app/src/main/res/drawable/ic_episodes.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000..9b3109d --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_location.xml b/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..6cece16 --- /dev/null +++ b/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_morty_smith.xml b/app/src/main/res/drawable/ic_morty_smith.xml new file mode 100644 index 0000000..ced3f0d --- /dev/null +++ b/app/src/main/res/drawable/ic_morty_smith.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..e787317 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/rick_prison.jpg b/app/src/main/res/drawable/rick_prison.jpg new file mode 100644 index 0000000..37f4346 Binary files /dev/null and b/app/src/main/res/drawable/rick_prison.jpg differ diff --git a/app/src/main/res/drawable/rickandmorty.png b/app/src/main/res/drawable/rickandmorty.png new file mode 100644 index 0000000..0cc0551 Binary files /dev/null and b/app/src/main/res/drawable/rickandmorty.png differ diff --git a/app/src/main/res/drawable/rickandmortytitle.png b/app/src/main/res/drawable/rickandmortytitle.png new file mode 100644 index 0000000..31613c9 Binary files /dev/null and b/app/src/main/res/drawable/rickandmortytitle.png differ diff --git a/app/src/main/res/drawable/search_view_background.xml b/app/src/main/res/drawable/search_view_background.xml new file mode 100644 index 0000000..8ec35ec --- /dev/null +++ b/app/src/main/res/drawable/search_view_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/abril_fatface.xml b/app/src/main/res/font/abril_fatface.xml new file mode 100644 index 0000000..2299e59 --- /dev/null +++ b/app/src/main/res/font/abril_fatface.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/anton.xml b/app/src/main/res/font/anton.xml new file mode 100644 index 0000000..2c6425b --- /dev/null +++ b/app/src/main/res/font/anton.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/archivo_black.xml b/app/src/main/res/font/archivo_black.xml new file mode 100644 index 0000000..d707493 --- /dev/null +++ b/app/src/main/res/font/archivo_black.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/bowlby_one.xml b/app/src/main/res/font/bowlby_one.xml new file mode 100644 index 0000000..dc72d9b --- /dev/null +++ b/app/src/main/res/font/bowlby_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/bowlby_one_sc.xml b/app/src/main/res/font/bowlby_one_sc.xml new file mode 100644 index 0000000..e6fdb52 --- /dev/null +++ b/app/src/main/res/font/bowlby_one_sc.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/bungee.xml b/app/src/main/res/font/bungee.xml new file mode 100644 index 0000000..465b0f5 --- /dev/null +++ b/app/src/main/res/font/bungee.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/candal.xml b/app/src/main/res/font/candal.xml new file mode 100644 index 0000000..1d2cbfe --- /dev/null +++ b/app/src/main/res/font/candal.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/erica_one.xml b/app/src/main/res/font/erica_one.xml new file mode 100644 index 0000000..0e54546 --- /dev/null +++ b/app/src/main/res/font/erica_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/passion_one.xml b/app/src/main/res/font/passion_one.xml new file mode 100644 index 0000000..8e72ea9 --- /dev/null +++ b/app/src/main/res/font/passion_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/russo_one.xml b/app/src/main/res/font/russo_one.xml new file mode 100644 index 0000000..0ccc478 --- /dev/null +++ b/app/src/main/res/font/russo_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/squada_one.xml b/app/src/main/res/font/squada_one.xml new file mode 100644 index 0000000..60211d9 --- /dev/null +++ b/app/src/main/res/font/squada_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/titan_one.xml b/app/src/main/res/font/titan_one.xml new file mode 100644 index 0000000..11dcb62 --- /dev/null +++ b/app/src/main/res/font/titan_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..be5b7a8 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/container_not_found.xml b/app/src/main/res/layout/container_not_found.xml new file mode 100644 index 0000000..a8457fa --- /dev/null +++ b/app/src/main/res/layout/container_not_found.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_characters.xml b/app/src/main/res/layout/fragment_characters.xml new file mode 100644 index 0000000..c9777b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_characters.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_episodes.xml b/app/src/main/res/layout/fragment_episodes.xml new file mode 100644 index 0000000..be1f872 --- /dev/null +++ b/app/src/main/res/layout/fragment_episodes.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_locations.xml b/app/src/main/res/layout/fragment_locations.xml new file mode 100644 index 0000000..5b765bc --- /dev/null +++ b/app/src/main/res/layout/fragment_locations.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 0000000..bb1a3f4 --- /dev/null +++ b/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_characters.xml b/app/src/main/res/layout/item_characters.xml new file mode 100644 index 0000000..6893369 --- /dev/null +++ b/app/src/main/res/layout/item_characters.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_episodes.xml b/app/src/main/res/layout/item_episodes.xml new file mode 100644 index 0000000..0e6443f --- /dev/null +++ b/app/src/main/res/layout/item_episodes.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_load_footer.xml b/app/src/main/res/layout/item_load_footer.xml new file mode 100644 index 0000000..7f94cb5 --- /dev/null +++ b/app/src/main/res/layout/item_load_footer.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_locations.xml b/app/src/main/res/layout/item_locations.xml new file mode 100644 index 0000000..5c96314 --- /dev/null +++ b/app/src/main/res/layout/item_locations.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_navigation_menu.xml b/app/src/main/res/menu/bottom_navigation_menu.xml new file mode 100644 index 0000000..b03e8c8 --- /dev/null +++ b/app/src/main/res/menu/bottom_navigation_menu.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_menu.xml b/app/src/main/res/menu/toolbar_menu.xml new file mode 100644 index 0000000..6c675c4 --- /dev/null +++ b/app/src/main/res/menu/toolbar_menu.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..c348272 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..96c462f --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..2e4125a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,15 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #393c42 + #1f232a + #0C0D0E + #00b2c9 + #9e9f9a + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..2f1e4b2 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,17 @@ + + + 150dp + 10dp + 17sp + 25sp + 10sp + 12sp + 4dp + 14dp + 2dp + 18dp + 38dp + 17dp + 3dp + 52dp + \ No newline at end of file diff --git a/app/src/main/res/values/font_certs.xml b/app/src/main/res/values/font_certs.xml new file mode 100644 index 0000000..d2226ac --- /dev/null +++ b/app/src/main/res/values/font_certs.xml @@ -0,0 +1,17 @@ + + + + @array/com_google_android_gms_fonts_certs_dev + @array/com_google_android_gms_fonts_certs_prod + + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + + + MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK + + + diff --git a/app/src/main/res/values/preloaded_fonts.xml b/app/src/main/res/values/preloaded_fonts.xml new file mode 100644 index 0000000..433779e --- /dev/null +++ b/app/src/main/res/values/preloaded_fonts.xml @@ -0,0 +1,18 @@ + + + + @font/ + @font/abril_fatface + @font/anton + @font/archivo_black + @font/bowlby_one + @font/bowlby_one_sc + @font/bungee + @font/candal + @font/erica_one + @font/passion_one + @font/russo_one + @font/squada_one + @font/titan_one + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..644177c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,27 @@ + + Rick & Morty + Персонажи + Локации + Эпизоды + Поиск + + Hello blank fragment + Rick & Morty + Jonatan Gray + Alive + Нет подключения к Интернету! + RETRY + Last known location: + Human + Earth + First seen in: + Type: + Dimension: + Created: + Was created in: + Episode + The air date was: + Поиск... + No results found + 404 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..f053fdf --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/example/rickandmortyapp/ExampleUnitTest.kt b/app/src/test/java/com/example/rickandmortyapp/ExampleUnitTest.kt new file mode 100644 index 0000000..cafea45 --- /dev/null +++ b/app/src/test/java/com/example/rickandmortyapp/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.rickandmortyapp + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..37bdb5c --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext { + kotlin_version = "1.6.0" + nav_version = "2.3.5" + hilt_version = '2.38.1' + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.0.4" + + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // Hilt + classpath("com.google.dagger:hilt-android-gradle-plugin:$hilt_version") + + // Navigation Safe Args + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..98bed16 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..92600bf --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jan 04 17:55:50 ALMT 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..addfb21 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + jcenter() // Warning: this repository is going to shut down soon + } +} +rootProject.name = "RickAndMortyApp" +include ':app'