LiveData or StateFlow skipping some states when used with Databinding - kotlin-coroutines

Layout
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.example.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/llNoDataLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:executionResult="#{viewmodel.state}"/>
</layout>
ViewModel
class MyViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle
): ViewModel(){
private val _state = MutableStateFlow<ExecutionResult<List<glucometerData>>>(ExecutionResult.idle())
val state: StateFlow<ExecutionResult<List<glucometerData>>> = _state
}
**Binding Adapter **
#BindingAdapter("executionResult")
fun View.visibility(state: ExecutionResult<List<Data>>?){
println("View.visibility:: Execution Result: state: $state")
when(state){
is ExecutionResult.Data -> {
this.isVisible = state.data.isNotEmpty()
}
is ExecutionResult.Error -> {
}
is ExecutionResult.Idle -> {
}
is ExecutionResult.Loading -> {
when(state.isLoading){
true -> this.isVisible = false
}
}
}
}
I am getting data from the repository. It will set Execution result in the following order: progress = true, data, progress = false.
In the binding adpater, I am getting only the Idle state and progress = false. Rest of the state in between is getting skipped.
Same thing is happening if I replace StateFlows with LiveData

Related

Creating a System Overlay > Can't interact items on screen on Chromebook Android 11

My app creates a system overlay (Always on Top over all Apps).
It works well on Android device version 11, Chromebook device with Android 9.
But It doesn't work on Chromebook Android 11: can't interact items on screen as this video demo
Source code
Following code:
Main configs:
type: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
Flags: WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
Detail code:
class SampleService : Service() {
lateinit var frameView: View
lateinit var windowManager: WindowManager
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
setupUi()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
private fun setupUi() {
listOf(
R.layout.chromeos_view
).forEach { id ->
AsyncLayoutInflater(this).inflate(id, null) { view, resId, _ ->
when (resId) {
R.layout.chromeos_view -> {
frameView = view
windowManager.addView(frameView, getLayoutParams())
}
}
}
}
}
private fun getLayoutParams(): WindowManager.LayoutParams {
val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
return WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
type,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.START or Gravity.TOP
x = 0
y = 0
alpha = 0.8F
}
}
}
Thank you for any helpful information you can offer, it's very much appreciated!

Handling commands from the viewmodel to the UI

The peculiarity of this application is that every time a user does something (except common things like typing) the application must check with an authority that they are indeed allowed to perform that action.
For example, let us say that the user wishes to see their profile (which is on the top bar)
the Composable screen looks something like this:
#Composable
fun HomeScreen(
navController: NavController,
vm: HomeViewModel = hiltViewModel()
) {
val state = vm.state.value
val scaffoldState = rememberScaffoldState()
HomeScreen(state, scaffoldState, vm::process)
}
#Composable
fun HomeScreen(state: HomeState, scaffoldState: ScaffoldState, event: (HomeEvent) -> Unit) {
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text("Hello world")
},
actions = {
IconButton(onClick = {
event.invoke(HomeEvent.ShowProfile)
}) {
Icon(
painter = painterResource(id = R.drawable.ic_person),
contentDescription = stringResource(id = R.string.profile)
)
}
}
)
}
) {
}
}
the view model receives it like so:
#HiltViewModel
class HomeViewModel #Inject constructor(app: Application, private val checkAllowed: CheckAllowed): AndroidViewmodel(app) {
val state = mutableStateOf(HomeState.Idle)
fun process(event:HomeEvent) {
when(event) {
HomeEvent.ShowProfile -> {
state.value = HomeState.Loading
viewModelScope.launch {
try {
val allowed = checkAllowed(Permission.SeeProfile) //use case that checks if the action is allowed
if (allowed) {
} else {
}
} finally {
state.value = HomeState.Idle
}
}
}
}
}
}
I now have to send a command to the ui, to either show a snackbar with the error or navigate to the profile page.
I have read a number of articles saying that compose should have a state, and the correct way to do this is make a new state value, containing the response, and when the HomeScreen receives it , it will act appropriately and send a message back that it is ok
I assume something like this :
in the viewmodel
val command = mutableStateOf<HomeCommand>(HomeCommand.Idle)
fun commandExecuted() {
command.value = HomeCommand.Idle
}
and inside the HomeScreen
val command = vm.command.value
try {
when (command) {
is HomeCommand.ShowProfile -> navController.navigate("profile_screen")
is HomeCommand.ShowSnackbar -> scaffoldState.snackbarHostState.showSnackbar(command.message, "Dismiss", SnackbarDuration.Indefinite)
}
}finally {
vm.commandExecuted()
}
but the way I did it is using flows like so:
inside the viewmodel:
private val _commands = MutableSharedFlow<HomeCommand>(0, 10, BufferOverflow.DROP_LATEST)
val commands: Flow<HomeCommand> = _commands
and inside the HomeScreen:
LaunchedEffect(key1 = vm) {
this#ExecuteCommands.commands.collectLatest { command ->
when (command) {
is HomeCommand.ShowProfile -> navController.navigate("profile_screen")
is HomeCommand.ShowSnackbar -> scaffoldState.snackbarHostState.showSnackbar(command.message, "Dismiss", SnackbarDuration.Indefinite)
}
}
This seems to work, but I am afraid there may be a memory leak or something I'm missing that could cause problems
Is my approach correct? Should I change it to state as in the first example? can I make it better somehow?

Glide FileNotFoundException

سلام عليكم
I want to load image with Glide but I got this error :
java.io.FileNotFoundException: https://via.placeholder.com/600/1e5390
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:238)
at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:210)
at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java)
at com.bumptech.glide.load.data.HttpUrlFetcher.loadDataWithRedirects(HttpUrlFetcher.java:102)
at com.bumptech.glide.load.data.HttpUrlFetcher.loadData(HttpUrlFetcher.java:56)
at com.bumptech.glide.load.engine.SourceGenerator.startNextLoad(SourceGenerator.java:70)
at com.bumptech.glide.load.engine.SourceGenerator.startNext(SourceGenerator.java:63)
at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:310)
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:276)
at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:234)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:393)
This is the image url:
https://via.placeholder.com/600/1e5390
And this is the Glide code:
#BindingAdapter("imageUrl")
fun ImageView.bindImage( imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide.with(this.context)
.load(imgUri)
.apply(
RequestOptions()
.error(R.drawable.ic_broken_image)
)
.into(this)
}
}
And use it here :
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:imageUrl="#{photoObject.photoUrl}"
app:srcCompat="#drawable/ic_broken_image" />
Thank you :))
This solve my problem:
val theImage = GlideUrl(
imgUrl, LazyHeaders.Builder()
.addHeader("User-Agent", "5")
.build()
)
Add user agent header to Glide
Full code :
#BindingAdapter("imageUrl")
fun ImageView.bindImage( imgUrl: String?) {
val theImage = GlideUrl(
imgUrl, LazyHeaders.Builder()
.addHeader("User-Agent", "5")
.build()
)
theImage.let {
Glide.with(this.context)
.load(theImage)
.apply(
RequestOptions()
.error(R.drawable.ic_broken_image)
)
.into(object : CustomViewTarget<ImageView, Drawable>(this) {
override fun onLoadFailed(errorDrawable: Drawable?) {}
override fun onResourceCleared(placeholder: Drawable?) {}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
this#bindImage.setImageDrawable(resource)
}
}) }
}
#BindingAdapter("app:setPhoto")
fun ImageView.setPhoto(purl: String?) {
val theImage = GlideUrl(
purl,LazyHeaders.Builder()
.addHeader("User-Agent", "5")
.build()
)
theImage.let {
Glide.with(this.context)
.load(theImage)
.into(this)
}
}
My solution, get agent:
val agent = WebView(this.context).settings.userAgentString
val glideUrl = GlideUrl(
"url_here",
LazyHeaders.Builder().addHeader("User-Agent", agent).build()
)
Glide.with(context)
.asBitmap()
.load(glideUrl)
.into(this)

How do I debug SwiftUI AttributeGraph cycle warnings?

I'm getting a lot of AttributeGraph cycle warnings in my app that uses SwiftUI. Is there any way to debug what's causing it?
This is what shows up in the console:
=== AttributeGraph: cycle detected through attribute 11640 ===
=== AttributeGraph: cycle detected through attribute 14168 ===
=== AttributeGraph: cycle detected through attribute 14168 ===
=== AttributeGraph: cycle detected through attribute 44568 ===
=== AttributeGraph: cycle detected through attribute 3608 ===
The log is generated by (from private AttributeGraph.framework)
AG::Graph::print_cycle(unsigned int) const ()
so you can set symbolic breakpoint for print_cycle
and, well, how much it could be helpful depends on your scenario, but definitely you'll get error generated stack in Xcode.
For me this issue was caused by me disabling a text field while the user was still editing it.
To fix this, you must first resign the text field as the first responder (thus stopping editing), and then disable the text field.
I explain this more in this Stack Overflow answer.
For me, this issue was caused by trying to focus a TextField right before changing to the tab of a TabView containing the TextField.
It was fixed by simply focusing the TextField after changing the TabView tab.
This seems similar to what #wristbands was experiencing.
For me the issue was resolved by not using UIActivityIndicator... not sure why though. The component below was causing problems.
public struct UIActivityIndicator: UIViewRepresentable {
private let style: UIActivityIndicatorView.Style
/// Default iOS 11 Activity Indicator.
public init(
style: UIActivityIndicatorView.Style = .large
) {
self.style = style
}
public func makeUIView(
context: UIViewRepresentableContext<UIActivityIndicator>
) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
public func updateUIView(
_ uiView: UIActivityIndicatorView,
context: UIViewRepresentableContext<UIActivityIndicator>
) {}
}
#Asperi Here is a minimal example to reproduce AttributeGraph cycle:
import SwiftUI
struct BoomView: View {
var body: some View {
VStack {
Text("Go back to see \"AttributeGraph: cycle detected through attribute\"")
.font(.title)
Spacer()
}
}
}
struct TestView: View {
#State var text: String = ""
#State private var isSearchFieldFocused: Bool = false
var placeholderText = NSLocalizedString("Search", comment: "")
var body: some View {
NavigationView {
VStack {
FocusableTextField(text: $text, isFirstResponder: $isSearchFieldFocused, placeholder: placeholderText)
.foregroundColor(.primary)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
NavigationLink(destination: BoomView()) {
Text("Boom")
}
Spacer()
}
.onAppear {
self.isSearchFieldFocused = true
}
.onDisappear {
isSearchFieldFocused = false
}
}
}
}
FocusableTextField.swift based on https://stackoverflow.com/a/59059359/659389
import SwiftUI
struct FocusableTextField: UIViewRepresentable {
#Binding public var isFirstResponder: Bool
#Binding public var text: String
var placeholder: String = ""
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, placeholder: String = "", configuration: #escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
self.placeholder = placeholder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.placeholder = placeholder
view.autocapitalizationType = .none
view.autocorrectionType = .no
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
#objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
For me the issue was that I was dynamically loading the AppIcon asset from the main bundle. See this Stack Overflow answer for in-depth details.
I was using enum cases as tag values in a TabView on MacOS. The last case (of four) triggered three attributeGraph cycle warnings. (The others were fine).
I am now using an Int variable (InspectorType.book.typeInt instead of InspectorType.book) as my selection variable and the cycle warnings have vanished.
(I can demonstrate this by commenting out the offending line respectively by changing the type of my selection; I cannot repeat it in another app, so there's obviously something else involved; I just haven't been able to identify the other culprit yet.)

How to pass data from the navigationdrawer to Activity using safe-args?

Let's say we have a project like this one:
class MainActivity : AppCompatActivity() {
private lateinit var drawerLayout: DrawerLayout
private lateinit var appBarConfiguration : AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
drawerLayout = binding.drawerLayout
val navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)
// prevent nav gesture if not on start destination
navController.addOnDestinationChangedListener { nc: NavController, nd: NavDestination, bundle: Bundle? ->
if (nd.id == nc.graph.startDestination) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
}
NavigationUI.setupWithNavController(binding.navView, navController)
}
Link to a simple project: https://github.com/udacity/andfun-kotlin-android-trivia/blob/Step.11-Solution-Adding-Animation/app/src/main/java/com/example/android/navigation/MainActivity.kt
My file for navGraph contains fragments and also one Activity where I want to go if user select its title from the navigation drawer. I want to send some data to this Activity. How can I do it using safe-args?
I'm using:
apply plugin: "androidx.navigation.safeargs"
and
implementation 'androidx.navigation:navigation-fragment:2.0.0'
implementation 'androidx.navigation:navigation-ui:2.0.0'
Firstly, you'll have to go to the navigation graph and specify an argument for the activity destination. This can be done via the design UI or in XML like this;
<navigation>
<activity android:id="#+id/someActivity">
<argument
android:name="isEditMode"
app:argType="boolean"
android:defaultValue="false" />
</activity>
</navigation>
This snippet assumes you are passing a boolean to the activity.
At this point, you can build the project so that all required files are generated.
Then in the onClick of whatever Navigation menu item responsible for starting the activity, you pass in the data;
override boolean onNavigationItemSelected(menuItem: menuItem) {
val id = menuItem.itemId
when (id) {
R.id.openActivity -> {
val bundle = bundleOf("isEditMode" to false)
findNavController().navigate(R.id.someActivity, bundle)
}
}
return true
}
Then in your activity, get the pass data safely as;
val safeArguments: MyActivityArgs by navArgs()
val isEditMode = safeArgs.isEditMode)
Inside your onCreate method in MainActivity use this:
val navController = this.findNavController(R.id.myNavHostFragment)
navController.addOnDestinationChangedListener { controller, destination, arguments ->
when(destination.id) {
R.id.caixaFragment -> {
var myVar = someMethodThatBringsMyVarValue()
val argument = NavArgument.Builder().setDefaultValue(mayVar).build()
destination.addArgument("idMes", argument)
}
}
}
I got it from Ayxan Haqverdili in this question

Resources