Jetpack Composable doesn't react to changes - android

I've been trying to follow the only good example with support that I could find, but in my case, it doesn't work.
I have a ViewModel that talks to a #Model in Composable, and changes a loading: Bool according to a MutableLiveData<Boolean> but it doesn't recompose.
class LoaderViewModel : ViewModel() {
val loadingLiveData = MutableLiveData<Boolean>(false)
fun fetch() {
viewModelScope.launch {
val flow = flowOf("result")
.onStart {
loadingLiveData.value = true
delay(2000)
}
.onCompletion {
loadingLiveData.value = false
}
.collect {
// Do something with the result
}
}
}
}
class LoaderFragment : Fragment() {
private val viewModel: LoaderViewModel by viewModel()
#Model
class ActivityLoadingState(var loading: Boolean = false)
private val activityLoadingState = ActivityLoadingState()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FrameLayout(context ?: return null).apply {
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setContent {
Loader()
}
}
}
#Composable
fun Loader() = MaterialTheme {
val loadingModel = activityLoadingState
Container {
Center {
if (loadingModel.loading) {
CircularProgressIndicator(
color = Color(0xFFFF0000)
)
} else {
Container { }
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeUI()
viewModel.fetch()
}
private fun subscribeUI() {
viewModel.loadingLiveData.observe(viewLifecycleOwner) {
activityLoadingState.loading = it
}
}
}

What I am doing, is to have Flows in my ViewModel, and use function collectAsState() whitin composables.

Related

Fragment navigation bug Android 13

So basically I have an app where I move across different fragments by using NavController navigation between fragments. This is working on all Android versions < 13.
findNavController().navigate(R.id.step02Fragment)
The issue on Android 13 is that when I move from the Step05Fragment to the Step06Fragment the onPause and onResume methods of the Step05Fragment start to execute forever and ever, causing a StackOverflow error.
I have no idea why this is happening between these two steps and in this specific Android version. All other fragments do the navigation the exact same way also and this issue doesn't happen on them.
Any ideas?
Thanks
P.D These are the libraries versions, if this is a known issue on any of them
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-service:2.5.1"
implementation "androidx.navigation:navigation-runtime-ktx:2.5.2"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "androidx.navigation:navigation-ui-ktx:2.5.2"
implementation "androidx.navigation:navigation-dynamic-features-fragment:2.5.2"
Here is the code of the fragments
#AndroidEntryPoint
class Step05Fragment : Fragment() {
lateinit var binding: FragmentStep05Binding
private var initialSetup: Boolean = true
private val disposable = CompositeDisposable()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStep05Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getBoolean(Constants.ENABLE_PERMISSION)?.let {
initialSetup = it
}
setObservables()
setView()
}
override fun onDestroy() {
super.onDestroy()
disposable.dispose()
}
private fun setObservables() {
binding.btnEnable.clicks()
.map {
checkPermission()
}
.subscribe()
.disposeBy(disposable)
}
private fun setView() {
if (!initialSetup) {
binding.currentStep.visibility = View.GONE
}
}
private fun checkPermission() {
if (!PermissionsUtils.isReadExternalStoragePermissionGranted(requireContext())) {
requestPermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
handleNavigation()
}
}
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
handleNavigation()
} else {
checkPermission()
}
}
private fun handleNavigation() {
if (initialSetup) {
findNavController().navigate(R.id.step06Fragment)
} else {
activity?.finishAffinity()
}
}
}
#AndroidEntryPoint
class Step06Fragment : Fragment() {
lateinit var binding: FragmentStep06Binding
private var initialSetup: Boolean = true
private val disposable = CompositeDisposable()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStep06Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getBoolean(Constants.ENABLE_PERMISSION)?.let {
initialSetup = it
}
setObservables()
setView()
}
override fun onDestroy() {
super.onDestroy()
disposable.dispose()
}
private fun setObservables() {
binding.btnEnable.clicks()
.map {
checkPermission()
}
.subscribe()
.disposeBy(disposable)
}
private fun setView() {
if (!initialSetup) {
binding.currentStep.visibility = View.GONE
}
}
private fun checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!PermissionsUtils.isMediaLocationPermissionGranted(requireContext())) {
requestPermission.launch(Manifest.permission.ACCESS_MEDIA_LOCATION)
} else {
handleNavigation()
}
} else {
handleNavigation()
}
}
private val requestPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
handleNavigation()
} else {
checkPermission()
}
}
private fun handleNavigation() {
if (initialSetup) {
findNavController().navigate(R.id.step07Fragment)
} else {
activity?.finishAffinity()
}
}
After a while, found the answer
https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions
Seems like this weird behavior is because of this, added the following validation after updating my project dependencies to aim Android 13 and it worked just fine
private fun checkPermission() {
if (!PermissionsUtils.isReadExternalStoragePermissionGranted(requireContext())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermission.launch(Manifest.permission.READ_MEDIA_IMAGES)
} else {
requestPermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
handleNavigation()
}
}
this can be related to binding! I had some binding issues before. Can you try changing it to the old findViewById or another way?

The best practice to pop up a common error dialog in any destination with navigation component

I want to display an error dialog while receiving errors in any destination fragment's ViewModel.
Now, I created a BaseViewModel and a BaseFragment that does the stuff, and make all fragments and ViewModels extend it.
But it's hard to connect all destinations to the dialog's DialogFragment.
Should I leave the dialog out of the nav_graph.xml?
If you are using kotlin you can do it by using an extension function
inline fun Activity.alertDialog(
title: CharSequence? = null,
message: CharSequence? = null,
func: AlertDialogHelper.() -> Unit
) {
val dialogFragment = AlertDialogHelper(this, title, message).apply {
func()
}
val fragmentTransaction = (this as AppCompatActivity).supportFragmentManager.beginTransaction()
fragmentTransaction.let { dialogFragment.show(it, TAG) }
}
inline fun Fragment.alertDialog(
title: CharSequence? = null,
message: CharSequence? = null,
func: AlertDialogHelper.() -> Unit
) {
val dialogFragment = AlertDialogHelper(this.context!!, title, message).apply {
func()
}
val fragmentTransaction = (this).childFragmentManager.beginTransaction()
fragmentTransaction.let { dialogFragment.show(it, TAG) }
}
AlertDialogHelper is the same dialog class that is extended by DialogFragment
AlertDialogHelper
class AlertDialogHelper(context: Context, title: CharSequence?, message: CharSequence?) :
DialogFragment() {
private val dialogView: View by lazyFast {
LayoutInflater.from(context).inflate(R.layout.dialog_layout, null)
}
private val title: TextView by lazyFast {
dialogView.findViewById(R.id.dialogInfoTitleTextView)
}
private val message: TextView by lazyFast {
dialogView.findViewById(R.id.dialogInfoMessageTextView)
}
private val positiveButton: Button by lazyFast {
dialogView.findViewById(R.id.dialogInfoPositiveButton)
}
private val negativeButton: Button by lazyFast {
dialogView.findViewById(R.id.dialogInfoNegativeButton)
}
var cancelable: Boolean? = true
init {
this.title.text = title
this.message.text = message
}
fun positiveButton(text: CharSequence, func: (() -> Unit)? = null) {
with(positiveButton) {
this.text = text
setClickListenerToDialogButton(func)
}
}
fun negativeButton(text: CharSequence, func: (() -> Unit)? = null) {
with(negativeButton) {
this.text = text
setClickListenerToDialogButton(func)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return dialogView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
title.goneIfTextEmpty()
message.goneIfTextEmpty()
positiveButton.goneIfTextEmpty()
negativeButton.goneIfTextEmpty()
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
isCancelable = this.cancelable!!
}
override fun onDestroyView() {
super.onDestroyView()
}
private fun TextView.goneIfTextEmpty() {
visibility = if (text.isNullOrEmpty()) {
View.GONE
} else {
View.VISIBLE
}
}
private fun Button.setClickListenerToDialogButton(func: (() -> Unit)?) {
setOnClickListener {
func?.invoke()
dialog?.dismiss()
}
}
fun <T> lazyFast(operation: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE) {
operation()
}
}

delay init methods in ViewModel

I have following project in Github : https://github.com/AliRezaeiii/TMDb-Paging
I have to postDelay calling methods in my ViewModel since datasource is not initialized :
abstract class DetailViewModel(private val item: TmdbItem) : BaseViewModel() {
private val handler = Handler(Looper.getMainLooper())
val trailers: ObservableList<Video> = ObservableArrayList()
val isTrailersVisible = ObservableBoolean(false)
private val _cast = MutableLiveData<List<Cast>>()
val cast: LiveData<List<Cast>> = _cast
val isCastVisible = ObservableBoolean(false)
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
protected abstract fun getTrailers(id: Int): Observable<List<Video>>
protected abstract fun getCast(id: Int): Observable<List<Cast>>
private fun showTrailers() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getTrailers(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ videos ->
if (videos.isNotEmpty()) {
isTrailersVisible.set(true)
}
with(trailers) {
clear()
addAll(videos)
}
}
) { throwable -> Timber.e(throwable) })
}
private fun showCast() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getCast(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ cast ->
if (cast.isNotEmpty()) {
isCastVisible.set(true)
}
this._cast.postValue(cast)
}
) { throwable -> Timber.e(throwable) })
}
}
And here is my Fragment :
abstract class DetailFragment<T : TmdbItem>
: BaseDaggerFragment(), CastClickCallback {
protected abstract fun getViewModel(): DetailViewModel
protected abstract fun getLayoutId(): Int
protected abstract fun initViewBinding(root: View): ViewDataBinding
protected abstract fun getTmdbItem(): T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val viewModel = getViewModel()
val root = inflater.inflate(getLayoutId(), container, false)
initViewBinding(root).apply {
setVariable(BR.vm, viewModel)
lifecycleOwner = viewLifecycleOwner
}
with(root) {
with(activity as AppCompatActivity) {
setupActionBar(details_toolbar) {
setDisplayShowTitleEnabled(false)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
summary_label.visibleGone(getTmdbItem().overview.trim().isNotEmpty())
// Make the MotionLayout draw behind the status bar
details_motion.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
summary.setOnClickListener {
val maxLine = resources.getInteger(R.integer.max_lines)
summary.maxLines = if (summary.maxLines > maxLine) maxLine else Int.MAX_VALUE
}
viewModel.cast.observe(viewLifecycleOwner, Observer {
it?.apply {
val adapter = CastAdapter(it, this#DetailFragment)
cast_list.apply {
setHasFixedSize(true)
cast_list.adapter = adapter
}
}
})
with(details_rv) {
postDelayed({ scrollTo(0, 0) }, 100)
}
}
return root
}
}
And BaseDaggerFragment :
open class BaseDaggerFragment : DaggerFragment() {
#Inject
lateinit var dataSource: RemoteDataSource
}
Could be any better solution than :
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
You can lazy initialize like this way
private val users:MutableLiveData<List<Cast>> by lazy {
MutableLiveData().also {
showTrailers()
showCast()
}
}
more details refer ViewModel

How to use Dagger with MVP in android

I want develop one application with Rxjava, Dagger, Kotin and MVP.
I write below codes but when run application show me nullPointerExecetpion error.
I know nullPointerExeception for my codes bug, but i try to found my bug i can't it!
My base fragment codes:
abstract class BaseFragment : Fragment(), BaseView {
var presenter: BasePresenter<*>? = null
abstract fun initializeDagger()
abstract fun initializePresenter()
abstract var layoutID: Int
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(layoutID, container, false)
initializeDagger()
initializePresenter()
return view
}
override fun onDetach() {
super.onDetach()
presenter?.onDestroy()
}
}
HomePresenter code:
class HomeTodayPresenter #Inject constructor(
val repositoryUseCase: RepositoryUseCase, disposable: CompositeDisposable
) : BasePresenter<HomeTodayView>(disposable) {
private var todayList = ArrayList<Today>()
fun onCreate() {
view?.initRepositoryList(todayList)
getTodayList()
}
fun onTodayRefreshList() {
getTodayList()
}
private fun getTodayList() {
compositeDisposable.add(
repositoryUseCase.getAuctionsToday()
.subscribe({ responseResult ->
view?.hideLoader()
responseResult?.let { itResponse ->
itResponse.res?.let { itRes ->
itRes.today?.let { itToday ->
if (itToday.size > 0) {
todayList.clear()
todayList.addAll(itToday)
view?.loadRepositoryList()
}
}
}
}
}, { e ->
view?.let { itView ->
e.message?.let { itErr ->
itView.showErrorMessage(itErr)
}
itView.hideLoader()
}
})
)
}
}
Home Fragment code:
class HomeTodayFragment : BaseFragment(), HomeTodayView {
#Inject
lateinit var homeTodayPresenter: HomeTodayPresenter
lateinit var todayAuctionsAdapter: TodayAuctionsAdapter
lateinit var layoutManager: LinearLayoutManager
private val swipeRefreshListener = SwipeRefreshLayout.OnRefreshListener {
homeTodayPresenter.onTodayRefreshList()
}
override fun initRepositoryList(list: ArrayList<Today>) {
layoutManager = LinearLayoutManager(requireContext())
todayAuctionsAdapter = TodayAuctionsAdapter(list)
requireContext().initRecyclerView(homeFragToday_list, layoutManager, todayAuctionsAdapter)
}
override fun loadRepositoryList() {
todayAuctionsAdapter.notifyDataSetChanged()
}
override fun hideLoader() {
homeFragToday_loader.visibility = View.GONE
}
override fun showErrorMessage(msg: String) {
Log.e("responseErr", msg)
}
override fun initializeDagger() {
AndroidInjection.inject(requireActivity())
}
override fun initializePresenter() {
super.presenter = homeTodayPresenter
homeTodayPresenter.view = this
}
override var layoutID: Int = R.layout.fragment_home_today
private lateinit var toolbarTile: TextView
lateinit var handler: Handler
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
homeTodayPresenter.onCreate()
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
activity?.let {
toolbarTile = it.findViewById(R.id.homePage_toolbarTitle)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
//Initialize
handler = Handler()
//Set delay
handler.postDelayed({
//Set title
toolbarTile.text = resources.getString(R.string.today)
toolbarTile.setTextColor(ResourcesCompat.getColor(resources, R.color.green_active, null))
}, 10)
}
}
}
LogCat error :
kotlin.UninitializedPropertyAccessException: lateinit property homeTodayPresenter has not been initialized
at com.app.applisttestapp.UI.Home.Fragments.Today.HomeTodayFragment.onCreateView(HomeTodayFragment.kt:69)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2539)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:875)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1227)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1293)
Show me error for this line : homeTodayPresenter.onCreate()
How can i fix it?

Initialization of a fragment causes the animation lag

While writing an Android app, I encountered a problem with a stuttering animation. I use AHBottomNavigation for navigation, FragNav is for swapping fragments and FlexibleAdapter for RecyclerView.
The application is built from one activity and five fragments. When I try to switch to the first fragment in the application, the BottomNavigation animation freez for a moment. It looks very unsightly. The second time I choose the same fragment, everything works smoothly. It seems to me that it is the fault to initialize the views in the fragment, but I have no idea how to do it differently.
AHBottomNavigation https://github.com/aurelhubert/ahbottomnavigation
FragNav https://github.com/ncapdevi/FragNav
FlexibleAdapter https://github.com/davideas/FlexibleAdapter
Fragment
class GradeFragment : BaseFragment(), GradeView {
#Inject
lateinit var presenter: GradePresenter
private val gradeAdapter = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
companion object {
fun newInstance() = GradeFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this#GradeFragment)
loadData()
}
}
override fun initView() {
gradeAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnUpdateListener { presenter.onUpdateDataList(it) }
setOnItemClickListener { position ->
getItem(position).let {
if (it is GradeItem) {
GradeDialog.newInstance(it.grade).show(fragmentManager, it.toString())
}
}
}
}
gradeRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeAdapter
}
gradeSwipe.setOnRefreshListener { presenter.loadData(forceRefresh = true) }
}
override fun updateData(data: List<GradeHeader>) {
gradeAdapter.updateDataSet(data, true)
}
override fun isViewEmpty(): Boolean = gradeAdapter.isEmpty
override fun showEmptyView(show: Boolean) {
gradeEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
gradeProgress.visibility = if (show) VISIBLE else GONE
}
override fun setRefresh(show: Boolean) {
gradeSwipe.isRefreshing = show
}
Presenter
class GradePresenter #Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersManager,
private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {
override fun attachView(view: GradeView) {
super.attachView(view)
view.initView()
}
fun loadData(forceRefresh: Boolean = false) {
disposable.add(sessionRepository.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { gradeRepository.getGrades(it, forceRefresh) }
.map { it.groupBy { grade -> grade.subject } }
.map { createGradeItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally { view?.setRefresh(false) }
.doOnSuccess { if (it.isEmpty()) view?.showEmptyView(true) }
.doOnError { view?.run { if (isViewEmpty()) showEmptyView(true) } }
.subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeHeader> {
return items.map {
val gradesAverage = calcAverage(it.value)
GradeHeader().apply {
subject = it.key
average = view?.run {
if (gradesAverage == 0f) emptyAverageString()
else averageString().format(gradesAverage)
}.orEmpty()
number = view?.gradeNumberString(it.value.size).orEmpty()
subItems = (it.value.map { item ->
GradeItem().apply {
grade = item
weightString = view?.weightString().orEmpty()
valueColor = getValueColor(item.value)
}
})
}
}
}
fun onUpdateDataList(size: Int) {
if (size != 0) view?.showProgress(false)
}
After a few days, I managed to solve the problem by updating the SDK to version 28. RecyclerView no longer causes animation jams when inflating

Categories

Resources