Skip to content

Commit a16ee7a

Browse files
Merge branch 'feature/home-ui' into develop
2 parents b2cf969 + f548a39 commit a16ee7a

28 files changed

Lines changed: 743 additions & 39 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.smarttoolfactory.propertyfindar
2+
3+
const val ORDER_BY_NONE = com.smarttoolfactory.data.constant.ORDER_BY_NONE
4+
const val ORDER_BY_PRICE_ASCENDING = com.smarttoolfactory.data.constant.ORDER_BY_PRICE_ASCENDING
5+
const val ORDER_BY_PRICE_DESCENDING = com.smarttoolfactory.data.constant.ORDER_BY_PRICE_DESCENDING
6+
const val ORDER_BY_BEDS_ASCENDING = com.smarttoolfactory.data.constant.ORDER_BY_BEDS_ASCENDING
7+
const val ORDER_BY_DES_DESCENDING = com.smarttoolfactory.data.constant.ORDER_BY_DES_DESCENDING

app/src/main/res/values-night/themes.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<resources xmlns:tools="http://schemas.android.com/tools">
22
<!-- Base application theme. -->
3-
<style name="Theme.PropertyFindAR" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
3+
<style name="Theme.PropertyFindAR" parent="Theme.MaterialComponents.DayNight.NoActionBar">
44
<!-- Primary brand color. -->
55
<item name="colorPrimary">@color/purple_200</item>
66
<item name="colorPrimaryVariant">@color/purple_700</item>

app/src/main/res/values/themes.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<resources xmlns:tools="http://schemas.android.com/tools">
22
<!-- Base application theme. -->
3-
<style name="Theme.PropertyFindAR" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
3+
<style name="Theme.PropertyFindAR" parent="Theme.MaterialComponents.DayNight.NoActionBar">
44
<!-- Primary brand color. -->
55
<item name="colorPrimary">@color/purple_500</item>
66
<item name="colorPrimaryVariant">@color/purple_700</item>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,127 @@
11
package com.smarttoolfactory.home
22

3+
import android.widget.Toast
4+
import androidx.fragment.app.Fragment
5+
import androidx.fragment.app.activityViewModels
6+
import androidx.lifecycle.Observer
7+
import androidx.navigation.NavController
8+
import androidx.navigation.ui.AppBarConfiguration
9+
import androidx.navigation.ui.setupWithNavController
10+
import androidx.viewpager2.adapter.FragmentStateAdapter
11+
import androidx.viewpager2.widget.ViewPager2
12+
import com.google.android.material.tabs.TabLayout
13+
import com.google.android.material.tabs.TabLayoutMediator
314
import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment
15+
import com.smarttoolfactory.core.util.Event
16+
import com.smarttoolfactory.core.viewmodel.NavControllerViewModel
17+
import com.smarttoolfactory.home.adapter.HomeViewPager2FragmentStateAdapter
418
import com.smarttoolfactory.home.databinding.FragmentHomeBinding
519

20+
/**
21+
* Fragment that contains [ViewPager2] and [TabLayout].
22+
* If this fragments get replaced and [Fragment.onDestroyView]
23+
* is called there are things to be considered
24+
*
25+
* * [FragmentStateAdapter] that is not null after [Fragment.onDestroy] cause memory leak,
26+
* so assign null to it
27+
*
28+
* * [TabLayoutMediator] cause memory leak if not detached after [Fragment.onDestroy]
29+
* of this fragment is called.
30+
*
31+
* * Data-binding which is not null after [Fragment.onDestroy] causes memory leak
32+
*
33+
* *[NavControllerViewModel] that has a [NavController] that belong to a NavHostFragment
34+
* that is to be destroyed also causes memory leak.
35+
*/
636
class HomeFragment : DynamicNavigationFragment<FragmentHomeBinding>() {
737

838
override fun getLayoutRes(): Int = R.layout.fragment_home
39+
40+
private val navControllerViewModel by activityViewModels<NavControllerViewModel>()
41+
42+
override fun bindViews() {
43+
44+
// ViewPager2
45+
val viewPager = dataBinding!!.viewPager
46+
47+
// TabLayout
48+
val tabLayout = dataBinding!!.tabLayout
49+
50+
/*
51+
Set Adapter for ViewPager inside this fragment using this Fragment,
52+
more specifically childFragmentManager as param
53+
54+
🔥 Create FragmentStateAdapter with viewLifeCycleOwner
55+
https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2
56+
*/
57+
viewPager.adapter =
58+
HomeViewPager2FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle)
59+
60+
dataBinding!!.toolbar.inflateMenu(R.menu.menu_home)
61+
62+
dataBinding!!.toolbar.setOnMenuItemClickListener {
63+
if (it.itemId == R.id.menu_item_sort) {
64+
Toast.makeText(requireContext(), "Toast", Toast.LENGTH_SHORT).show()
65+
true
66+
}
67+
68+
false
69+
}
70+
71+
// Bind tabs and viewpager
72+
TabLayoutMediator(tabLayout, viewPager, tabConfigurationStrategy).attach()
73+
74+
subscribeAppbarNavigation()
75+
}
76+
77+
private fun subscribeAppbarNavigation() {
78+
navControllerViewModel.currentNavController.observe(
79+
viewLifecycleOwner,
80+
Observer { it ->
81+
82+
it?.let { event: Event<NavController?> ->
83+
event.getContentIfNotHandled()?.let { navController ->
84+
val appBarConfig = AppBarConfiguration(navController.graph)
85+
dataBinding!!.toolbar.setupWithNavController(navController, appBarConfig)
86+
}
87+
}
88+
}
89+
)
90+
}
91+
92+
override fun onDestroyView() {
93+
94+
// ViewPager2
95+
val viewPager2 = dataBinding!!.viewPager
96+
// TabLayout
97+
val tabLayout = dataBinding!!.tabLayout
98+
99+
/*
100+
🔥 Detach TabLayoutMediator since it causing memory leaks when it's in a fragment
101+
https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2
102+
*/
103+
TabLayoutMediator(tabLayout, viewPager2, tabConfigurationStrategy).detach()
104+
105+
/*
106+
🔥 Without setting ViewPager2 Adapter to null it causes memory leak
107+
https://stackoverflow.com/questions/62851425/viewpager2-inside-a-fragment-leaks-after-replacing-the-fragment-its-in-by-navig
108+
*/
109+
viewPager2?.let {
110+
it.adapter = null
111+
}
112+
113+
// Remove menu item click listener
114+
dataBinding!!.toolbar.setOnMenuItemClickListener(null)
115+
116+
super.onDestroyView()
117+
}
118+
119+
private val tabConfigurationStrategy =
120+
TabLayoutMediator.TabConfigurationStrategy { tab, position ->
121+
when (position) {
122+
0 -> tab.text = "Flow"
123+
1 -> tab.text = "RxJava3"
124+
else -> tab.text = "Flow+Pagination"
125+
}
126+
}
9127
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.smarttoolfactory.home.adapter
2+
3+
import androidx.fragment.app.Fragment
4+
import androidx.fragment.app.FragmentManager
5+
import androidx.lifecycle.Lifecycle
6+
import com.smarttoolfactory.core.ui.adapter.NavigableFragmentStateAdapter
7+
import com.smarttoolfactory.core.ui.fragment.navhost.NavHostContainerFragment
8+
import com.smarttoolfactory.home.R
9+
10+
/**
11+
* FragmentStateAdapter to contain ViewPager2 fragments inside another fragment.
12+
*
13+
* * 🔥 Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure
14+
* that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive
15+
*
16+
* * https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2
17+
*/
18+
class HomeViewPager2FragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
19+
NavigableFragmentStateAdapter(fragmentManager, lifecycle) {
20+
21+
override fun getItemCount(): Int = 3
22+
23+
override fun createFragment(position: Int): Fragment {
24+
25+
return when (position) {
26+
27+
// Fragment with Flow
28+
0 -> NavHostContainerFragment.createNavHostContainerFragment(
29+
R.layout.fragment_navhost_property_list_flow,
30+
R.id.nested_nav_host_fragment_property_list
31+
)
32+
33+
// Fragment with Pagination
34+
else -> NavHostContainerFragment.createNavHostContainerFragment(
35+
R.layout.fragment_navhost_property_list_rxjava3,
36+
R.id.nested_nav_host_fragment_property_list
37+
)
38+
}
39+
}
40+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.smarttoolfactory.home.adapter
2+
3+
import android.widget.ImageButton
4+
import androidx.annotation.LayoutRes
5+
import androidx.databinding.ViewDataBinding
6+
import androidx.recyclerview.widget.DiffUtil
7+
import androidx.recyclerview.widget.RecyclerView
8+
import com.smarttoolfactory.core.ui.adapter.BaseListAdapter
9+
import com.smarttoolfactory.domain.model.PropertyItem
10+
import com.smarttoolfactory.home.BR
11+
import com.smarttoolfactory.home.R
12+
import com.smarttoolfactory.home.databinding.RowPropertyBinding
13+
14+
class PropertyItemListAdapter(
15+
@LayoutRes private val layoutId: Int,
16+
private val onItemClicked: ((PropertyItem) -> Unit)? = null,
17+
private val onLikeButtonClicked: ((PropertyItem) -> Unit)? = null
18+
) :
19+
BaseListAdapter<PropertyItem>(
20+
layoutId,
21+
PropertyItemDiffCallback()
22+
) {
23+
24+
override fun onViewHolderBound(
25+
binding: ViewDataBinding,
26+
item: PropertyItem,
27+
position: Int,
28+
payloads: MutableList<Any>
29+
) {
30+
binding.setVariable(BR.item, item)
31+
}
32+
33+
/**
34+
* Add click listener here to prevent setting listener after a ViewHolder every time
35+
* ViewHolder is scrolled and onBindViewHolder is called
36+
*/
37+
override fun onViewHolderCreated(
38+
viewHolder: RecyclerView.ViewHolder,
39+
viewType: Int,
40+
binding: ViewDataBinding
41+
) {
42+
43+
binding.root.setOnClickListener {
44+
onItemClicked?.let {
45+
it((getItem(viewHolder.bindingAdapterPosition)))
46+
}
47+
}
48+
49+
if (binding is RowPropertyBinding) {
50+
51+
binding.ivLike.setOnClickListener { likeButton ->
52+
onLikeButtonClicked?.let { onLikeButtonClick ->
53+
54+
getItem(viewHolder.bindingAdapterPosition).apply {
55+
// Change like status of PropertyItem
56+
isFavorite = !isFavorite
57+
onLikeButtonClick(this)
58+
59+
// Set image source of like button
60+
val imageResource = if (isFavorite) {
61+
R.drawable.ic_baseline_favorite_24
62+
} else {
63+
R.drawable.ic_baseline_favorite_border_24
64+
}
65+
(likeButton as? ImageButton)?.setImageResource(imageResource)
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
/**
74+
* Callback for calculating the diff between two non-null items in a list.
75+
*
76+
* Used by ListAdapter to calculate the minimum number of changes between and old list and a new
77+
* list that's been passed to `submitList`.
78+
*/
79+
class PropertyItemDiffCallback : DiffUtil.ItemCallback<PropertyItem>() {
80+
81+
override fun areItemsTheSame(
82+
oldItem: PropertyItem,
83+
newItem: PropertyItem
84+
): Boolean {
85+
return oldItem == newItem
86+
}
87+
88+
override fun areContentsTheSame(
89+
oldItem: PropertyItem,
90+
newItem: PropertyItem
91+
): Boolean {
92+
return oldItem.id == newItem.id
93+
}
94+
}

features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListFlowFragment.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.smarttoolfactory.home.propertylist
22

33
import android.os.Bundle
4+
import androidx.core.os.bundleOf
5+
import androidx.recyclerview.widget.LinearLayoutManager
46
import com.smarttoolfactory.core.di.CoreModuleDependencies
57
import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment
68
import com.smarttoolfactory.home.R
9+
import com.smarttoolfactory.home.adapter.PropertyItemListAdapter
710
import com.smarttoolfactory.home.databinding.FragmentPropertyListBinding
811
import com.smarttoolfactory.home.di.DaggerHomeComponent
912
import com.smarttoolfactory.home.viewmodel.PropertyListViewModelFlow
@@ -24,7 +27,45 @@ class PropertyListFlowFragment : DynamicNavigationFragment<FragmentPropertyListB
2427
}
2528

2629
override fun bindViews() {
27-
super.bindViews()
30+
dataBinding.viewModel = viewModel
31+
32+
dataBinding.recyclerView.apply {
33+
34+
// Set Layout manager
35+
this.layoutManager =
36+
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
37+
38+
// Set RecyclerViewAdapter
39+
this.adapter =
40+
PropertyItemListAdapter(
41+
R.layout.row_property,
42+
viewModel::onClick,
43+
viewModel::onLikeButtonClick
44+
45+
)
46+
}
47+
48+
val swipeRefreshLayout = dataBinding.swipeRefreshLayout
49+
50+
swipeRefreshLayout.setOnRefreshListener {
51+
swipeRefreshLayout.isRefreshing = false
52+
viewModel.refreshPropertyList()
53+
}
54+
55+
subscribeGoToDetailScreen()
56+
}
57+
58+
private fun subscribeGoToDetailScreen() {
59+
60+
viewModel.goToDetailScreen.observe(
61+
viewLifecycleOwner,
62+
{
63+
64+
it.getContentIfNotHandled()?.let { propertyItem ->
65+
val bundle = bundleOf("property" to propertyItem)
66+
}
67+
}
68+
)
2869
}
2970

3071
private fun initCoreDependentInjection() {

0 commit comments

Comments
 (0)