diff --git a/app/build.gradle b/app/build.gradle index 43bd303..f47d934 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,6 +43,7 @@ android { buildFeatures { viewBinding = true + dataBinding = true } } @@ -55,7 +56,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - + implementation 'androidx.recyclerview:recyclerview:1.2.1' // networking implementation 'com.squareup.moshi:moshi:1.14.0' implementation 'com.squareup.moshi:moshi-kotlin:1.14.0' @@ -95,6 +96,9 @@ dependencies { implementation("com.google.firebase:firebase-crashlytics-ktx") implementation("com.google.firebase:firebase-analytics-ktx") + // lottie implementation 'com.airbnb.android:lottie:6.0.0' + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" + } \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index b2ae6d6..0000000 Binary files a/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/app/src/main/java/com/example/rickmorty/BindingAdapter.kt b/app/src/main/java/com/example/rickmorty/BindingAdapter.kt new file mode 100644 index 0000000..5ce144a --- /dev/null +++ b/app/src/main/java/com/example/rickmorty/BindingAdapter.kt @@ -0,0 +1,16 @@ +package com.example.rickmorty + +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import com.squareup.picasso.Picasso + + +//In Kotlin, the let function is a scoping function that allows you to perform operations on a non-null object within a safe context. +// It is particularly useful for executing a block of code only if the object is not null. +@BindingAdapter("setImage") +fun bindImage(imgView: ImageView, imgUrl: String?) { + // it will only execute if imageUrl is non null + imgUrl?.let { + Picasso.get().load(imgUrl).into(imgView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rickmorty/HomeActivity.kt b/app/src/main/java/com/example/rickmorty/HomeActivity.kt index ce8e462..2be3c00 100644 --- a/app/src/main/java/com/example/rickmorty/HomeActivity.kt +++ b/app/src/main/java/com/example/rickmorty/HomeActivity.kt @@ -1,5 +1,7 @@ package com.example.rickmorty +import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.drawerlayout.widget.DrawerLayout @@ -42,8 +44,19 @@ class HomeActivity : AppCompatActivity() { navController.graph.startDestinationId ) + + // about dev menu item + val menu = findViewById(R.id.nav_view).menu + val aboutDev = menu.findItem(R.id.about_dev) + aboutDev?.setOnMenuItemClickListener { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://harisheoran.github.io")) + startActivity(intent) + true + } } + + // support back navigation override fun onSupportNavigateUp(): Boolean { return navController.navigateUp(appBarConfiguration) diff --git a/app/src/main/java/com/example/rickmorty/characters/CharacterListAdapter.kt b/app/src/main/java/com/example/rickmorty/characters/CharacterListAdapter.kt new file mode 100644 index 0000000..344bcd3 --- /dev/null +++ b/app/src/main/java/com/example/rickmorty/characters/CharacterListAdapter.kt @@ -0,0 +1,46 @@ +package com.example.rickmorty.characters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.example.rickmorty.databinding.ModelCharacterListItemBinding +import com.example.rickmorty.network.response.GetCharacterByIdResponse + +class CharacterListAdapter(private val onCharacterClick: (Int) -> Unit) : + PagingDataAdapter(DiffCallback) { + + class CharacterViewHolder(var binding: ModelCharacterListItemBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(onCharacterClick: (Int) -> Unit, character: GetCharacterByIdResponse) { + binding.character = character + binding.executePendingBindings() + binding.root.setOnClickListener { + onCharacterClick(character.id) + } + } + } + + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view = CharacterViewHolder(ModelCharacterListItemBinding.inflate(LayoutInflater.from(parent.context))) + return view + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val myCharacter = getItem(position) + (holder as? CharacterViewHolder)?.bind(onCharacterClick, myCharacter!!) + } + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: GetCharacterByIdResponse, newItem: GetCharacterByIdResponse): Boolean { + return oldItem.name == newItem.name + } + + override fun areContentsTheSame(oldItem: GetCharacterByIdResponse, newItem: GetCharacterByIdResponse): Boolean { + return oldItem.name == newItem.name + } + } +} + diff --git a/app/src/main/java/com/example/rickmorty/characters/CharacterListFragment.kt b/app/src/main/java/com/example/rickmorty/characters/CharacterListFragment.kt index 531ac02..1f14fe3 100644 --- a/app/src/main/java/com/example/rickmorty/characters/CharacterListFragment.kt +++ b/app/src/main/java/com/example/rickmorty/characters/CharacterListFragment.kt @@ -4,14 +4,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.widget.ContentLoadingProgressBar import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import com.airbnb.epoxy.EpoxyRecyclerView -import com.airbnb.lottie.LottieAnimationView +import androidx.paging.LoadState +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.example.rickmorty.NetworkViewModel import com.example.rickmorty.R +import com.google.android.material.button.MaterialButton import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -25,6 +29,8 @@ class CharacterListFragment : Fragment() { ViewModelProvider(this).get(CharacterListViewModel::class.java) } + private lateinit var characterAdapter: CharacterListAdapter + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -35,39 +41,55 @@ class CharacterListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - view.findViewById(R.id.loading).visibility = View.VISIBLE - view.findViewById(R.id.no_internet).visibility = View.GONE - - networkViewModel = ViewModelProvider(this).get(NetworkViewModel::class.java) - - - networkViewModel.getNetworkState().observe(viewLifecycleOwner, { - if (it) { - view.findViewById(R.id.loading).visibility = View.GONE - view.findViewById(R.id.epoxy_character_recycler_view).visibility = View.VISIBLE - view.findViewById(R.id.no_internet).visibility = View.GONE - lifecycleScope.launch { - viewModel.pagingDataFlow.collectLatest { - epoxyController.submitData(it) - } - } - } else { - view.findViewById(R.id.loading).visibility = View.GONE - view.findViewById(R.id.epoxy_character_recycler_view).visibility = View.GONE - view.findViewById(R.id.no_internet).visibility = View.VISIBLE + characterAdapter = CharacterListAdapter(::onCharacterClicked) + + initViewModelData() + initRecyclerView() + + } + + private fun initViewModelData() { + lifecycleScope.launch { + viewModel.pagingDataFlow.collectLatest { + characterAdapter.submitData(it) } - }) + } + } + + private fun initRecyclerView() { + view?.findViewById(R.id.character_recycler_view)?.adapter = + characterAdapter.withLoadStateHeaderAndFooter( + header = PagingLoadStateAdapter { characterAdapter.retry() }, + footer = PagingLoadStateAdapter { characterAdapter.retry() } + ) + + + characterAdapter.addLoadStateListener { loadState -> + view?.findViewById(R.id.swipe_refresh)?.isRefreshing = + loadState.refresh is LoadState.Loading + view?.findViewById(R.id.character_recycler_view)?.isVisible = + loadState.source.refresh is LoadState.NotLoading + view?.findViewById(R.id.progress_bar)?.isVisible = + loadState.source.refresh is LoadState.Loading + view?.findViewById(R.id.retry_button)?.isVisible = + loadState.source.refresh is LoadState.Error + + } + + view?.findViewById(R.id.swipe_refresh)?.setOnRefreshListener { + characterAdapter.refresh() + } + + view?.findViewById(R.id.retry_button)?.setOnClickListener { + characterAdapter.retry() + } - view.findViewById(R.id.epoxy_character_recycler_view) - .setController(epoxyController) } - private fun onCharacterClicked(characterId: Int) { - val action = CharacterListFragmentDirections.actionCharacterListFragmentToCharacterFragment( - characterId - ) + private fun onCharacterClicked(characterId: Int) { + val action = CharacterListFragmentDirections.actionCharacterListFragmentToCharacterFragment(characterId) findNavController().navigate(action) } } \ No newline at end of file diff --git a/app/src/main/java/com/example/rickmorty/characters/CharacterListPagingDataSource.kt b/app/src/main/java/com/example/rickmorty/characters/CharacterListPagingDataSource.kt index 2fdf02e..9728b22 100644 --- a/app/src/main/java/com/example/rickmorty/characters/CharacterListPagingDataSource.kt +++ b/app/src/main/java/com/example/rickmorty/characters/CharacterListPagingDataSource.kt @@ -37,6 +37,10 @@ class CharacterListPagingDataSource( } override fun getRefreshKey(state: PagingState): Int? { - TODO("Not yet implemented") + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/rickmorty/characters/CharacterListViewModel.kt b/app/src/main/java/com/example/rickmorty/characters/CharacterListViewModel.kt index 7007915..19975e1 100644 --- a/app/src/main/java/com/example/rickmorty/characters/CharacterListViewModel.kt +++ b/app/src/main/java/com/example/rickmorty/characters/CharacterListViewModel.kt @@ -27,5 +27,4 @@ class CharacterListViewModel : ViewModel() { val pagingDataFlow: Flow> = pager.flow.cachedIn(viewModelScope) - } \ No newline at end of file diff --git a/app/src/main/java/com/example/rickmorty/characters/PagingLoadStateAdapter.kt b/app/src/main/java/com/example/rickmorty/characters/PagingLoadStateAdapter.kt new file mode 100644 index 0000000..75946a0 --- /dev/null +++ b/app/src/main/java/com/example/rickmorty/characters/PagingLoadStateAdapter.kt @@ -0,0 +1,48 @@ +package com.example.rickmorty.characters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.widget.ContentLoadingProgressBar +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.rickmorty.R +import com.google.android.material.button.MaterialButton +import com.google.android.material.textview.MaterialTextView + +class PagingLoadStateAdapter(val retryCallback: () -> Unit) : + LoadStateAdapter() { + + class LoadStateViewHolder(itemView: View, retry: () -> Unit) : RecyclerView.ViewHolder(itemView) { + + init { + itemView.findViewById(R.id.retry_button).setOnClickListener { + retry.invoke() + } + } + + val progressBar = itemView.findViewById(R.id.progress_bar) + val errorText = itemView.findViewById(R.id.error_msg) + fun bind(loadState: LoadState) { + if (loadState is LoadState.Error) { + errorText.text = "Try Again later..." + } + progressBar.isVisible = loadState is LoadState.Loading + itemView.findViewById(R.id.retry_button).isVisible = loadState !is LoadState.Error + errorText.isVisible = loadState !is LoadState.Loading + } + } + + override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) { + holder.bind(loadState) + } + + override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder { + return LoadStateViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.model_network_state, parent, false), + retry = retryCallback + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/appicon.jpg b/app/src/main/res/drawable/appicon.jpg deleted file mode 100644 index e6aaac3..0000000 Binary files a/app/src/main/res/drawable/appicon.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/episode_number_bg.xml b/app/src/main/res/drawable/episode_number_bg.xml index ff7fe9d..94e6961 100644 --- a/app/src/main/res/drawable/episode_number_bg.xml +++ b/app/src/main/res/drawable/episode_number_bg.xml @@ -3,7 +3,7 @@ - + diff --git a/app/src/main/res/drawable/info_24.xml b/app/src/main/res/drawable/info_24.xml new file mode 100644 index 0000000..e0ecb40 --- /dev/null +++ b/app/src/main/res/drawable/info_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/main.png b/app/src/main/res/drawable/main.png new file mode 100644 index 0000000..852cd73 Binary files /dev/null and b/app/src/main/res/drawable/main.png differ diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 041058d..ba868ae 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -28,7 +28,7 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" - android:fitsSystemWindows="true" + android:fitsSystemWindows="false" app:headerLayout="@layout/header_drawer_layout" app:menu="@menu/menu_drawer"/> diff --git a/app/src/main/res/layout/fragment_character__list.xml b/app/src/main/res/layout/fragment_character__list.xml index ad03e2d..7f78cbf 100644 --- a/app/src/main/res/layout/fragment_character__list.xml +++ b/app/src/main/res/layout/fragment_character__list.xml @@ -1,42 +1,51 @@ - - - + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> - + android:layout_height="match_parent"> + + + + + + - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/header_drawer_layout.xml b/app/src/main/res/layout/header_drawer_layout.xml index ea7408e..9bdb24e 100644 --- a/app/src/main/res/layout/header_drawer_layout.xml +++ b/app/src/main/res/layout/header_drawer_layout.xml @@ -1,8 +1,47 @@ + android:layout_height="150dp"> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/model_character_list_item.xml b/app/src/main/res/layout/model_character_list_item.xml index e141a80..2973fb3 100644 --- a/app/src/main/res/layout/model_character_list_item.xml +++ b/app/src/main/res/layout/model_character_list_item.xml @@ -1,34 +1,47 @@ - - - + + + + + + app:cardCornerRadius="8dp" + app:cardPreventCornerOverlap="true" + app:cardElevation="4dp" + app:cardUseCompatPadding="true" + android:layout_margin="16dp" + android:elevation="16dp" - + android:layout_height="wrap_content"> - + android:layout_height="match_parent"> + + + + - + - + + diff --git a/app/src/main/res/layout/model_episodes_page.xml b/app/src/main/res/layout/model_episodes_page.xml index 7675177..934d6db 100644 --- a/app/src/main/res/layout/model_episodes_page.xml +++ b/app/src/main/res/layout/model_episodes_page.xml @@ -7,8 +7,8 @@ android:layout_height="80dp" app:cardCornerRadius="8dp" app:cardElevation="8dp" - app:strokeColor="?colorOnSurface" - app:cardBackgroundColor="@color/md_theme_light_primaryContainer"> + app:strokeColor="@color/black" + app:cardBackgroundColor="?colorPrimaryContainer"> + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml index eea96b3..3a82544 100644 --- a/app/src/main/res/menu/menu_drawer.xml +++ b/app/src/main/res/menu/menu_drawer.xml @@ -1,5 +1,5 @@ - + @@ -20,4 +20,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d6a5923..dbd2b32 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,65 +1,65 @@ #FF000000 - #f9bf1e - #785A00 + #344955 + #006688 #FFFFFF - #FFDF9C - #251A00 - #006971 + #C2E8FF + #001E2C + #845400 #FFFFFF - #82F3FF - #002022 - #944B00 + #FFDDB6 + #2A1800 + #006684 #FFFFFF - #FFDCC5 - #301400 + #BDE9FF + #001F2A #BA1A1A #FFDAD6 #FFFFFF #410002 - #FFFBFF - #2A1800 - #FFFBFF - #2A1800 - #EDE1CF - #4D4639 - #7F7667 - #FFEEDE - #462A00 - #F8BE1C + #FAFCFF + #001F2A + #FAFCFF + #001F2A + #DCE3E9 + #40484D + #71787D + #E1F4FF + #003547 + #77D1FF #000000 - #785A00 - #D0C5B4 + #006688 + #C0C7CD #000000 - #F8BE1C - #3F2E00 - #5B4300 - #FFDF9C - #4DD9E6 - #00363B - #004F55 - #82F3FF - #FFB783 - #4F2500 - #703700 - #FFDCC5 + #77D1FF + #003548 + #004D68 + #C2E8FF + #FFB959 + #462A00 + #643F00 + #FFDDB6 + #66D3FF + #003546 + #004D64 + #BDE9FF #FFB4AB #93000A #690005 #FFDAD6 - #2A1800 - #FFDDB6 - #2A1800 - #FFDDB6 - #4D4639 - #D0C5B4 - #999080 - #2A1800 - #FFDDB6 - #785A00 + #001F2A + #BFE9FF + #001F2A + #BFE9FF + #40484D + #C0C7CD + #8A9297 + #001F2A + #BFE9FF + #006688 #000000 - #F8BE1C - #4D4639 + #77D1FF + #40484D #000000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a42eef..80223d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Rick&Morty + Rick & Morty Hello blank fragment \ No newline at end of file