This is my main activity of my app. I'm using nav controller.
class MainActivity : AppCompatActivity() {
lateinit var viewModel: NewsViewModel
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val newRepository = NewsRepository(ArticleDatabase(this))
val viewModelProviderFactory = NewsViewModelProviderFactory(newRepository)
viewModel = ViewModelProvider(this , viewModelProviderFactory).get(NewsViewModel::class.java)
// bottomNavigationView.setupWithNavController(newsNavHostFragment.findNavController())
val navHostFragment= supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
navController= navHostFragment.navController
bottomNavigationView.setupWithNavController(navController)
}
This the breaking news fragment. The function is same in other fragments so only posting this one
class BreakingNewsFragment : Fragment (R.layout.fragment_breaking_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setUpRecyclerView()
newsAdapter!!.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article" , it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment2 , bundle
)
}
viewModel.breakingNews.observe(viewLifecycleOwner, Observer { response ->
when(response) {
is Resource.Success -> {
hideProgressBar()
response.data?.let { newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles)
}
}
is Resource.Error -> {
hideProgressBar()
response.message?.let { message ->
Log.e("Breaking Fragment", "An error occured: $message")
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
}
private fun hideProgressBar() {
paginationProgressBar.visibility = View.INVISIBLE
}
private fun showProgressBar() {
paginationProgressBar.visibility = View.VISIBLE
}
fun setUpRecyclerView() {
newsAdapter = NewsAdapter()
rvBreakingNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
}
}
This is made from Youtuber Philip lackner's tutorial. Here's the the link of original project
https://github.com/philipplackner/MVVMNewsApp . His version of fragment is deprecated so i used new fragment view in xml file. so there is an issue in using nav controller.
EDIT -> Forgot to put errors. Here it is
2022-10-17 01:08:09.796 3971-3971/com.arpit.newsapp20 E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.arpit.newsapp20, PID: 3971
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.hashCode()' on a null object reference
at com.arpit.newsapp20.models.Article.hashCode(Unknown Source:15)
at androidx.navigation.NavBackStackEntry.hashCode(NavBackStackEntry.kt:256)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at androidx.navigation.NavController.linkChildToParent(NavController.kt:143)
at androidx.navigation.NavController.addEntryToBackStack(NavController.kt:1918)
at androidx.navigation.NavController.addEntryToBackStack$default(NavController.kt:1813)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1721)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1719)
at androidx.navigation.NavController$NavControllerNavigatorState.push(NavController.kt:287)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:198)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:164)
at androidx.navigation.NavController.navigateInternal(NavController.kt:260)
at androidx.navigation.NavController.navigate(NavController.kt:1719)
at androidx.navigation.NavController.navigate(NavController.kt:1545)
at androidx.navigation.NavController.navigate(NavController.kt:1472)
at androidx.navigation.NavController.navigate(NavController.kt:1454)
at com.arpit.newsapp20.ui.BreakingNewsFragment$onViewCreated$1.invoke(BreakingNewsFragment.kt:31)
at com.arpit.newsapp20.ui.BreakingNewsFragment$onViewCreated$1.invoke(BreakingNewsFragment.kt:27)
at com.arpit.newsapp20.adapters.NewsAdapter.onBindViewHolder$lambda-2$lambda-1(NewsAdapter.kt:56)
at com.arpit.newsapp20.adapters.NewsAdapter.$r8$lambda$FmTlKYZBcoLQp02jU2NS9dL1z-k(Unknown Source:0)
at com.arpit.newsapp20.adapters.NewsAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7441)
at android.view.View.performClickInternal(View.java:7418)
at android.view.View.access$3700(View.java:835)
at android.view.View$PerformClick.run(View.java:28676)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7839)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Here is the article class
#Entity(
tableName = "articles"
)
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
val author: String,
val content: String?,
val description: String,
val publishedAt: String,
val source: Source,
val title: String,
val url: String,
val urlToImage: String
) : Serializable
Here is the news adapter class
class NewsAdapter : RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
inner class ArticleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
private val differCallback = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article_preview,
parent,
false
)
)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
private var onItemClickListener: ((Article) -> Unit)? = null
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply {
Glide.with(this).load(article.urlToImage).into(ivArticleImage)
tvSource.text = article.source.name
tvTitle.text = article.title
tvDescription.text = article.description
tvPublishedAt.text = article.publishedAt
setOnClickListener {
onItemClickListener?.let { it(article) }
}
}
}
fun setOnItemClickListener(listener: (Article) -> Unit) {
onItemClickListener = listener
}
}
You look like to want to pass the clicked news item to the article fragment. But in your click listener you don't receive the clicked item. Instead you pass something else in "it". I think that's why you get a null pointer exception.
You need something like following:
newsAdapter!!.setOnItemClickListener {
val newsItem = // get clicked news item from adapter
newsItem?.let {
val bundle = Bundle().apply {
putSerializable("article" , it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment2 , bundle
)
}
}
Related
I have an API which give me the list of doctors. On it's last page only 1 item is there and other items are null like this:
After this i have used paging library for pagination
my pagingSource code: `
class DocPagingSource(val docRepository: DocRepository): PagingSource<Int, Data>() {
override fun getRefreshKey(state: PagingState<Int, Data>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
return try {
val currentPage = params.key?: 1
val city: String = ""
val response = docRepository.getDoctors(city, currentPage)
val page = Math.ceil(response.body()!!.total.toDouble()/5).toInt()
val data = response.body()!!.data
val responseData = mutableListOf<Data>()
responseData.addAll(data)
LoadResult.Page(
data = responseData,
prevKey = if(currentPage==1) null else -1,
nextKey = if (currentPage== page) null else currentPage.plus(1)
)
}catch (e: HttpException){
LoadResult.Error(e)
}catch (e: Exception){
LoadResult.Error(e)
}
}
`
My paging Adapter Code:
class DocAdapter(val context: Context): PagingDataAdapter<Data, DocAdapter.DocViewHolder>(DiffUtil()) {
private lateinit var binding: ItemDoctorsBinding
inner class DocViewHolder : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Data?) {
binding.apply {
txtDocCity.text = item?.city
txtDocName.text = item?.docName
txtDocFees.text = item?.docConsultationFee
txtDocYOE.text = item?.docYoE
txtDocSpecialisation.text = item?.docSpecialisation
Glide.with(context)
.load(item?.docProfileImgUrl)
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(docPhoto)
}
}
}
override fun onBindViewHolder(holder: DocViewHolder, position: Int) {
val item = getItem(position)
if (item!=null){
holder.bind(getItem(position)!!)
holder.setIsRecyclable(false)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocViewHolder {
val inflater = LayoutInflater.from(context)
binding = ItemDoctorsBinding.inflate(inflater, parent, false)
return DocViewHolder()
}
class DiffUtil: androidx.recyclerview.widget.DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.docId == newItem.docId
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem==newItem
}
}}
what I am getting after reaching my 16th item in doctor list on last page it should show entry till 16th item but after that it also shows again like this:
Also if i dont use holder.setIsRecyclable(false) in pagingAdapter then this android icon not shown but then list is populated with previous doctors:
on the top DR. Sixteen is shown like this:
and in between it again shows like this:
My doctorViewModel Class:
class DocViewModel(val repository: DocRepository): ViewModel() {
val loading = MutableLiveData<Boolean>()
val docList = Pager(PagingConfig(5, maxSize = 100)){
DocPagingSource(repository)
}.flow.cachedIn(viewModelScope)}
My main Activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var docViewModel: DocViewModel
private lateinit var docAdapter: DocAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val docListRepository = DocRepository()
val docFactory = DocViewModelFactory(docListRepository)
docViewModel = ViewModelProvider(this, docFactory).get(DocViewModel::class.java)
docAdapter = DocAdapter(this)
lifecycleScope.launchWhenCreated {
docViewModel.docList.collect{
docAdapter.submitData(it)
}
}
binding.docRecyclerView.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = docAdapter
setHasFixedSize(true)
}
}}
I have solved this error by applying a condition in my paging source code
My app crashes when I attempt to click on articles published days ago, however it works fine when I tried to do it on a more recent article, here is an image for reference.
The app crashes when I scroll down and click on past news, the app also seems to be quite laggy and unresponsive, would appreciate any advice.
The error seems to lie on the Onclicklistener portion of the code, I uploaded all the codes which I think are relevant.
Runtime error
From the errors below, the classes highlighted were the BreakingNews and the NewsAdapter.
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Object.hashCode()' on a null object reference
at com.example.thenewsapplication.ROOM.Source.hashCode(Unknown Source:2)
at com.example.thenewsapplication.ROOM.Article.hashCode(Unknown Source:71)
at androidx.navigation.NavBackStackEntry.hashCode(NavBackStackEntry.kt:256)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at androidx.navigation.NavController.linkChildToParent(NavController.kt:143)
at androidx.navigation.NavController.addEntryToBackStack(NavController.kt:1918)
at androidx.navigation.NavController.addEntryToBackStack$default(NavController.kt:1813)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1721)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1719)
at androidx.navigation.NavController$NavControllerNavigatorState.push(NavController.kt:287)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:198)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:164)
at androidx.navigation.NavController.navigateInternal(NavController.kt:260)
at androidx.navigation.NavController.navigate(NavController.kt:1719)
at androidx.navigation.NavController.navigate(NavController.kt:1545)
at androidx.navigation.NavController.navigate(NavController.kt:1472)
at androidx.navigation.NavController.navigate(NavController.kt:1454)
at com.example.thenewsapplication.Fragments.BreakingNews$onViewCreated$1.invoke(BreakingNews.kt:57)
at com.example.thenewsapplication.Fragments.BreakingNews$onViewCreated$1.invoke(BreakingNews.kt:53)
at com.example.thenewsapplication.Adapter.NewsAdapter.onBindViewHolder$lambda-2$lambda-1(NewsAdapter.kt:82)
at com.example.thenewsapplication.Adapter.NewsAdapter.$r8$lambda$xRXjhIuiNyf8fdAGPo8jTchti_k(Unknown Source:0)
at com.example.thenewsapplication.Adapter.NewsAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7448)
at android.view.View.performClickInternal(View.java:7425)
at android.view.View.access$3600(View.java:810)
at android.view.View$PerformClick.run(View.java:28305)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Article class
(The article itself)
I extended the serialization class so I can pass info between fragments via Bundle
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? =null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String,
val urlToImage: String?
) : Serializable
Source (for the source datatype in the Article data class)
data class Source(
val id: Any,
val name: String
)
TypeConverters (Convert the Source datatype to a primative type)
class Converters {
#TypeConverter
fun fromSource(source: Source): String{
return source.name
}
#TypeConverter
fun toSource(name: String): Source {
return Source(name, name)
}
}
Adapter (Adapter for the BreakingNews class RecyclerView)
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val ivArticleImage = itemView.findViewById<ImageView>(R.id.ivArticleImage)
val tvSource = itemView.findViewById<TextView>(R.id.tvSource)
val tvTitle = itemView.findViewById<TextView>(R.id.tvTitle)
val tvDescription = itemView.findViewById<TextView>(R.id.tvDescription)
val tvPublishedAt = itemView.findViewById<TextView>(R.id.tvPublishedAt)
}
private val diffcallback = object : DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, diffcallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article_preview,
parent,
false
)
)
}
fun setOnItemClickListener(listen: (Article) -> Unit){
Log.d("NewsAdapter", "setOnItem")
onItemClickListener = listen
}
private var onItemClickListener: ((Article) -> Unit)? = null
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply {
Glide.with(this).load(article.urlToImage).into(holder.ivArticleImage)
holder.tvSource.text = article.source?.name
holder.tvTitle.text = article.title
holder.tvDescription.text = article.description
holder.tvPublishedAt.text = article.publishedAt
setOnClickListener{
Log.d("NewsAdapter", "onBindViewHolder")
onItemClickListener?.let {
it(article)
}
}
}
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
BreakingNews (The Fragment/UI)
class BreakingNews: Fragment(R.layout.breakingnews) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
lateinit var binding: BreakingnewsBinding
val TAG = "BreakingNewsFragment"
var isLoading = false
var isLastPage = false
var isScrolling = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = BreakingnewsBinding.inflate(inflater,container,false);
val view = binding.root;
return view;
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setupRecyclerView()
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article",it) //Passing data between activity or fragments, work based on key/value pairs. Since we have denoted our Article Class to be of type Serializable, we can use this to pass data.
}
findNavController().navigate(
R.id.action_breakingNews_to_newsDetailsFragment,
bundle
)
}
viewModel.breakingNews.observe(viewLifecycleOwner,Observer{response ->
when (response){
is Resource.Success -> {
hideProgressBar()
response.data?.let {newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles.toList())
val totalPages = newsResponse.totalResults / QUERY_PAGE_SIZE + 2
isLastPage = viewModel.breakingNewsPage == totalPages
}
}
is Resource.Error -> {
showProgressBar()
response.message?.let {
Log.e(TAG,"An error occured $it")
}
}
is Resource.Loading -> {
hideProgressBar()
}
}
})
}
private fun hideProgressBar() {
binding.paginationProgressBar.visibility = View.INVISIBLE
isLoading = false
}
private fun showProgressBar() {
binding.paginationProgressBar.visibility = View.VISIBLE
isLoading = true
}
var scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
isScrolling = true
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val isNotLoadingAndNotLastPage = !isLoading && !isLastPage
val isAtLastItem = firstVisibleItemPosition + visibleItemCount >= totalItemCount
val isNotAtBeginning = firstVisibleItemPosition >= 0
val isTotalMoreThanVisible = totalItemCount >= QUERY_PAGE_SIZE
val shouldPaginate =
isNotLoadingAndNotLastPage && isAtLastItem && isNotAtBeginning && isTotalMoreThanVisible && isScrolling
if (shouldPaginate) {
viewModel.getBreakingNews("us")
isScrolling = false
}
}
}
private fun setupRecyclerView() {
newsAdapter = NewsAdapter()
binding.breakingNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
addOnScrollListener(this#BreakingNews.scrollListener)
}
}
}
enter code here
I think it's not about being old article and new article, it's about that articles have id of null. To prevent that organize your data classes like below:
#Entity(tableName = "articles")
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
val author: String?,
val content: String?,
val description: String?,
val publishedAt: String?,
val source: Source?,
val title: String?,
val url: String?,
val urlToImage: String?
): Serializable {
override fun hashCode(): Int {
var result = id.hashCode()
if(url.isNullOrEmpty()){
result = 31 * result + url.hashCode()
}
return result
}
}
data class Source(
val id: String,
val name: String
): Serializable {
override fun hashCode(): Int {
var result = id.hashCode()
if(name.isNullOrEmpty()){
result = 31 * result + name.hashCode()
}
return result
}
}
We have a random crash on production in this class when accessing the binding at line 10 :
class BulletinFragment : Fragment(R.layout.fragment_bulletins) {
private val bulletinViewModel: BulletinsViewModel by viewModel()
private val binding by viewBinding(FragmentBulletinsBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
bulletinViewModel.switchState.collect {
binding.bulletinLiveNotificationsBanner.switch.isSelected = it
}
}
}
}
}
viewModel is provided by Koin, and the binding delegate is Zhuiden's one from here
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var _binding: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_binding = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = _binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { _binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
): Lazy<T> {
return lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
}
This fragment is called within a viewPager2:
class CartPagerAdapter(fragment: Fragment) : FragmentStateAdapter(
fragment.childFragmentManager,
fragment.viewLifecycleOwner.lifecycle
) {
val firstFragment = FirstFragment()
val secondFragment = SecondFragment()
override fun createFragment(position: Int): Fragment = when (position) {
Tab.FIRST_TAB.tabIndex -> firstFragment
Tab.SECOND_TAB.tabIndex -> secondFragment
Tab.THIRD_TAB.tabIndex -> BulletinFragment()
else -> error("The fragment position should in 0 < x < 2 but was '$position'")
}
fun handleDeeplink(deeplink: Uri) {
when (deeplink.host) {
FIRST_TAB_DEEPLINK_HOST -> firstFragment.handleDeeplink(deeplink)
SECOND_TAB_DEEPLINK_HOST -> secondFragment.handleDeeplink(deeplink)
}
}
override fun getItemCount(): Int = 3
}
class CartHomeFragment : Fragment(R.layout.fragment_cart_home), CartHomeContract.View {
private var tabLayoutMediator: TabLayoutMediator? = null
private val args: CartHomeFragmentArgs by navArgs()
private var initTab: Int? = null
// betSlip needs to scroll to top when displaying QrCodes tab (set when moving to QrCode tab after validating cart)
private val pagerAdapter: CartPagerAdapter by adapter { CartPagerAdapter(this) }
private val binding by viewBinding(FragmentCartHomeBinding::bind)
private val scope
get() = viewLifecycleOwner.lifecycleScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initTab = args.tabIndex
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
override fun onDestroyView() {
tabLayoutMediator?.detach()
tabLayoutMediator = null
super.onDestroyView()
}
private fun initialize() {
scope.launch {
binding.fragmentCartHomeViewPager.run {
adapter = pagerAdapter
setPagerCurrentItem(this)
offscreenPageLimit = 2
}
tabLayoutMediator = TabLayoutMediator(binding.cartTabLayout, binding.fragmentCartHomeViewPager) { tab, position ->
val (title, contentDesc) = with(getTab(position)) { getString(title) to getString(contentDesc) }
tab.text = title
tab.contentDescription = contentDesc
}
tabLayoutMediator?.attach()
}
}
private fun setPagerCurrentItem(viewPager: ViewPager2) {
val initialIntent = arguments?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT)
initialIntent?.data?.let {
pagerAdapter.handleDeeplink(it)
viewPager.setCurrentItemForDeeplink(it.host)
initialIntent.data = null
} ?: run {
initTab?.let {
viewPager.setCurrentItem(it, false)
initTab = null
}
}
}
fun getTab(index: Int): Tab {
return Tab.values()[index]
}
private fun ViewPager2.setCurrentItemForDeeplink(deeplink: String?) {
setCurrentItem(Tab.getTabIndexForDeeplink(deeplink), false)
}
companion object {
const val FIRST_TAB_DEEPLINK_HOST = "first"
const val SECOND_TAB_DEEPLINK_HOST = "second"
const val THIRD_TAB_DEEPLINK_HOST = "third"
val DEFAULT_TAB: Tab = Tab.FIRST_TAB
const val SECOND_TAB_DEEPLINK_DETAILS_PATH = "/details"
}
}
enum class Tab(#StringRes val title: Int, #StringRes val contentDesc: Int, val deeplink: String) {
FIRST_TAB(R.string.first_tab_tab_title, R.string.a11y_first_tab, FIRST_TAB_DEEPLINK_HOST),
SECOND_TAB(R.string.second_tab_title, R.string.a11y_second_tab, SECOND_TAB_DEEPLINK_HOST),
THIRD_TAB(R.string.third_tab_title, R.string.a11y_third_tab, THIRD_TAB_DEEPLINK_HOST);
val tabIndex: Int = ordinal
companion object {
fun getTabIndexForDeeplink(deeplink: String?): Int =
(values().firstOrNull { it.deeplink == deeplink }
?: DEFAULT_TAB)
.tabIndex
}
}
In the BulletinFragment, I know that the repeatOnLifecycle block seems useless here but we need it for some reason that is not necessary to explain here. I just would like to understand what is wrong with this piece of code. Actually, we get from crashlytics the following crash happening randomly (rare enough to not succeed to reproduce it, but frequent enough to significantly decrease the crashfree):
Fatal Exception: java.lang.IllegalStateException Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
androidx.fragment.app.Fragment.getViewLifecycleOwner (Fragment.java:377)
com.mycompany.myapp.common.tools.FragmentViewBindingDelegate.getValue (FragmentViewBindingDelegate.kt:40)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.<clinit> (BulletinFragment.kt:18)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.access$getBinding (BulletinFragment.java:15)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:25)
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:24)
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2.emit (Emitters.kt:227)
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2$1.invokeSuspend (Emitters.kt:12)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.internal.DispatchedContinuation.resumeWith (DispatchedContinuation.kt:205)
kotlin.coroutines.SafeContinuation.resumeWith (SafeContinuationJvm.kt:41)
How can we endup with this crash when
we tie the coroutine with the viewLifecycleOwner lifecycleScope
and the collect is done inside a block where the lifecycleOwner state is STARTED, the lifecycleOwner being the view if I properly understand.
How the view can be null in this case ??? Is it related to the ViewPager2
I m getting data from two End points using flows and assigning those two list to temporary list in ViewModel. For this purpose, I'm using combine function and returning result as stateFlows with stateIn operator but that's not working. Can anyone point me out where I go wrong please.
ViewModel.kt
private val _movieItem: MutableStateFlow<State<List<HomeRecyclerViewItems>>> =
MutableStateFlow(State.Loading())
val movieItems: StateFlow<State<List<HomeRecyclerViewItems>>> = _movieItem
fun getHomeItemList() {
viewModelScope.launch {
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList // This is not working as "+" Unresolve Error
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
State.loading<Nothing>()
) as State<List<HomeRecyclerViewItems>> // Unchecked cast: StateFlow<Any> to State<List<HomeRecyclerViewItems>>
}
Repository.kt
fun getMovieList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Movie>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Movie>, List<HomeRecyclerViewItems.Movie>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Movie>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Movie>> =
apiInterface.getMoviesList()
}.asFlow()
}
fun getDirectorList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Directors>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Directors>, List<HomeRecyclerViewItems.Directors>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Directors>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Directors>> =
apiInterface.getDirectorsList()
}.asFlow()
}
Network BoundRepository.kt
#ExperimentalCoroutinesApi
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<ResponseAPI<REQUEST>> {
val apiResponse = fetchFromRemote()
val remotePosts = apiResponse.body()
if (apiResponse.isSuccessful && remotePosts != null) {
emit(ResponseAPI.Success(remotePosts))
} else {
emit(ResponseAPI.Failed(apiResponse.errorBody()!!.string()))
}
}.catch { e ->
e.printStackTrace()
emit(ResponseAPI.Failed("Server Problem! Please try again Later. "))
}
#WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)
#MainThread
protected abstract fun fetchFromLocal()
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
Endpoints with Sealed Class
#GET("directors")
fun getDirectorsList(): Response<List<HomeRecyclerViewItems.Directors>>
#GET("movies")
fun getMoviesList(): Response<List<HomeRecyclerViewItems.Movie>>
sealed class HomeRecyclerViewItems {
class Title(
val id: Int,
val title: String
) : HomeRecyclerViewItems()
class Movie(
val id: Int,
val title: String,
val thumbnail: String,
val releaseDate: String
) : HomeRecyclerViewItems()
class Directors(
val id: Int,
val name: String,
val avator: String,
val movie_count: Int
) : HomeRecyclerViewItems()
}
Fragment.kt
#AndroidEntryPoint
#ExperimentalCoroutinesApi
class SettingsFragment : BaseBottomTabFragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<SettingViewModel>()
#Inject
lateinit var recyclerViewAdapter: RecyclerViewAdapter
#Inject
lateinit var bundle: Bundle
var finalList = mutableListOf<HomeRecyclerViewItems>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentSettingsBinding.inflate(layoutInflater,container,false)
val view = binding.root
binding.rvMovie.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(activity)
}
bundle.putString("Hello","hihg")
Toast.makeText(activity, "${bundle.getString("Hello")}", Toast.LENGTH_SHORT).show()
finalList.add(HomeRecyclerViewItems.Title(1,"hello"))
return view
}
private fun observeList() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
launch {
viewModel.movieItems.collect { state ->
when(state){
is State.Loading ->{
}
is State.Success->{
if (state.data.isNotEmpty()){
recyclerViewAdapter = RecyclerViewAdapter()
binding.rvMovie.adapter = recyclerViewAdapter
recyclerViewAdapter.submitList(finalList)
}
}
is State.Error -> {
Toast.makeText(activity, "Error", Toast.LENGTH_SHORT).show()
}
else -> Unit
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as MainActivity).binding.ivSearch.isGone = true
viewModel.getHomeItemList()
observeList()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Note: I m following this tutorial simpliedCoding for api data for multirecyclerview but want to implement it with Kotlin State Flow. Any help in this regard is highly appreciated. Thanks.
Your problem is in here
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList
}
They are not returning a List<HomeRecyclerViewItems>, but a State<List<HomeRecyclerViewItems>. Maybe a better name for the variables are testsState and directorsState. After that it will be more clear why you need to unpack the values before combining the lists
_movieItem.value = combine(testsState, directorsState) { testsState, directorsState ->
val homeRecyclerViewItems = mutableListOf<HomeRecyclerViewItems>()
if (testsState is Success) homeRecyclerViewItems.add(testsState.data)
if (directorsState is Success) homeRecyclerViewItems.add(directorsState.data)
homeRecyclerViewItems
}
Here is my first time to apply MVVM concept to my Android Application. I follow the steps at the referenced article
https://medium.com/swlh/realtime-firestore-pagination-on-android-with-mvvm-b5e30cea437
And I am managed to load data successfully. When it comes to implementing the onclick event at the row of my RecyclerView List, it comes out that there has no onlick response .
Would you please suggest the better method to implement the onCLick method given that I have applied PageListAdapter?
When I study the PageListAdapter documentation on Android, it seems no clue for me to implement the onclick method.
class MovieViewModel(movieRepository: MovieRepository) : ViewModel() {
private val viewModelJob = SupervisorJob()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
var selected: MutableLiveData<RealtimeMovie>? = null
private val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.setPageSize(20)
.build()
val records: LiveData<PagedList<RealtimeMovie>> =
LivePagedListBuilder<String, RealtimeMovie>(
MovieDataSource.Factory(movieRepository, uiScope),
config
).build()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here is my adapter:
class MovieAdapter : PagedListAdapter<RealtimeMovie, MovieAdapter.MovieViewHolder>(
object : DiffUtil.ItemCallback<RealtimeMovie>() {
override fun areItemsTheSame(old: RealtimeMovie, new: RealtimeMovie): Boolean =
old.id == new.id
override fun areContentsTheSame(old: RealtimeMovie, new: RealtimeMovie): Boolean =
old == new
}
) {
private lateinit var onItemClick: (movie: RealtimeMovie) -> Unit
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.list_movie,
parent,
false
)
return MovieViewHolder(view)
}
infix fun setOnItemClick(onClick: (movie: RealtimeMovie) -> Unit) {
this.onItemClick = onClick
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val record = getItem(position)
holder.bind(record)
holder.itemView.setOnClickListener { onItemClick(record!!) }
}
override fun onViewRecycled(holder: MovieViewHolder) {
super.onViewRecycled(holder)
holder.apply {
txtRecordName.text = ""
crdRecord.isEnabled = true
crdRecord.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
android.R.color.white
)
)
viewHolderDisposables.clear()
}
}
inner class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val viewHolderDisposables = CompositeDisposable()
val crdRecord by lazy { view.findViewById<MaterialCardView>(R.id.crd_record) }
val txtRecordName by lazy { view.findViewById<TextView>(R.id.txt_record_name) }
fun bind(RealtimeMovie: RealtimeMovie?) {
RealtimeMovie?.let {
it.record
.subscribeBy(
onNext = {
txtRecordName.text = it.title
},
onError = {
// Handle error here
// Record maybe deleted
}
)
.addTo(viewHolderDisposables)
}
}
}
}
Here is my fragment :
..
viewModel = ViewModelProviders.of(this, factory).get(MovieViewModel::class.java)
viewModel.records.observe(this, Observer {
swpRecords.isRefreshing = false
recordsAdapter.submitList(it)
recordsAdapter.setOnItemClick {
print("movie : ${it.id}" )
print("movie : ${it.record}" )
}
})