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?
Related
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!
I think that the problem is I don't know to use well the Coroutines. In Maps Activity you'll see that I access to a PointsDao suspend function that returns a List of objects that I want to use to create marks at my Google Maps Activity.
#AndroidEntryPoint
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
private lateinit var binding: ActivityMapsBinding
private lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>
private val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val mapsViewModel: MapsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMapsBinding.inflate(layoutInflater)
setContentView(binding.root)
requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) {
permissions ->
if (permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false)) {
Log.d("fine_location", "Permission granted")
} else {
Log.d("fine_location", "Permission not granted")
getBackToMainActivity()
Toast.makeText(this, "Necessites acceptar els permisos de geolocalització per a realitzar la ruta", Toast.LENGTH_LONG).show()
}
if (permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)) {
Log.d("coarse_location", "Permission granted")
} else {
Log.d("coarse_location", "Permission not granted")
getBackToMainActivity()
Toast.makeText(this, "Necessites acceptar els permisos de geolocalització per a realitzar la ruta", Toast.LENGTH_LONG).show()
}
}
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
requestLocationPermissions()
}
/**
* Manipulates the map once available.
* This callback is triggered when the map is ready to be used.
* This is where we can add markers or lines, add listeners or move the camera.
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
CoroutineScope(Dispatchers.Main).launch {
val listOfPoints = getRoutePoints()
for (point in listOfPoints) {
mMap.addMarker(MarkerOptions().position(LatLng( point.latitude, point.longitude)))
if (point == listOfPoints[0]) {
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(LatLng(point.latitude, point.longitude), 18f))
}
}
}
}
private fun requestLocationPermissions() {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) -> {
Log.d("fine_location", "Permission already granted")
}
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) -> {
Log.d("coarse_location", "Permission already granted")
}
else -> {
requestPermissionLauncher.launch(permissions)
}
}
}
private fun getBackToMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
private fun getRouteId(): Int {
return intent.getIntExtra("routeId", 0)
}
// Gets the points from room repository through ViewModel
private fun getRoutePoints(): List<PointOfInterest> {
val route = getRouteId()
var points = emptyList<PointOfInterest>()
CoroutineScope(Dispatchers.IO).launch {
points = mapsViewModel.getRoutePoints(route)
}
return points
}
This is my ViewModel for this Activity:
#HiltViewModel
class MapsViewModel #Inject constructor(private val repository: RoomRepository): ViewModel() {
suspend fun getRoutePoints(routeId: Int): List<PointOfInterest> {
return repository.getPointsByRouteId(routeId)
}
}
And the Dao:
#Dao
interface PointsDao
{
#Query("SELECT * FROM points_tbl WHERE route_id = :routeId")
suspend fun getRoutePoints(routeId: Int): List<PointOfInterest>
}
My stracktrace error:
Process: com.buigues.ortola.touristics, PID: 27515
java.lang.IllegalStateException: Method addObserver must be called on the main thread
at androidx.lifecycle.LifecycleRegistry.enforceMainThreadIfNeeded(LifecycleRegistry.java:317)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:172)
at androidx.lifecycle.SavedStateHandleController.attachToLifecycle(SavedStateHandleController.java:49)
at androidx.lifecycle.SavedStateHandleController.create(SavedStateHandleController.java:70)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:67)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:84)
at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:109)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:171)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:139)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:44)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:31)
at com.buigues.ortola.touristics.ui.MapsActivity.getMapsViewModel(MapsActivity.kt:39)
at com.buigues.ortola.touristics.ui.MapsActivity.getRoutePoints(MapsActivity.kt:123)
at com.buigues.ortola.touristics.ui.MapsActivity.access$getRoutePoints(MapsActivity.kt:31)
at com.buigues.ortola.touristics.ui.MapsActivity$onMapReady$1.invokeSuspend(MapsActivity.kt:85)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
The problem is here in getRoutePoints().
CoroutineScope(Dispatchers.IO).launch {
points = mapsViewModel.getRoutePoints(route)
}
The by viewModels() in your ViewModel property does a lazy load of the ViewModel. As a result, if you access your ViewModel property for the first time when you are not on the main thread, it will try to create it on the wrong thread, triggering this crash. ViewModels must be constructed on the main thread.
CoroutineScope(Dispatchers.IO) means you are creating a coroutine scope that by default uses background IO threads, so this code is run on a background thread.
You should not be creating a CoroutineScope for this anyway, because your Activity already has one that is properly managed by the Activity lifecycle (so it will cancel any in-progress jobs if the activity is closed, to avoid wasting resources).
Also, getRoutePoints() is a suspend function. There's no reason for you to be using Dispatchers.IO here. A suspend function by convention is safe to call from any dispatcher. (It is however possible to write one that breaks convention, but Room is properly designed and does not break convention.)
To fix the crash and run a coroutine properly, you should use lifecycleScope.launch { //.... However, this function as you have designed it won't do what you expect. It launches a coroutine to retrieve a value, but then it immediately returns before that coroutine has finished running, so in this case will just return the initial emptyList(). When you launch a coroutine, you are queuing up background work, but the current function that called launch continues synchronously without waiting for the coroutine results. If it did, it would be a blocking function. There's more information about that in my answer here.
So, you should instead make this a suspend function:
// Gets the points from room repository through ViewModel
private suspend fun getRoutePoints(): List<PointOfInterest> {
val route = getRouteId()
return mapsViewModel.getRoutePoints(route)
}
And your onMapReady function should also be fixed to use proper scope:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
lifecycleScope.launch {
val listOfPoints = getRoutePoints()
for (point in listOfPoints) {
mMap.addMarker(MarkerOptions().position(LatLng( point.latitude, point.longitude)))
if (point == listOfPoints[0]) {
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(LatLng(point.latitude, point.longitude), 18f))
}
}
}
}
The Item.kt class is
#Entity(tableName = "item")
class Item(
val id: Long,
val title: String,
) {
#Ignore
var selection: Boolean = false
}
Then i make a query to get all the items in the table ,it return
LiveData<List<Item>>
Then in the viewModel i want to apply selection(true) accordig to the Mutablelivedata selectionId, the selection id contain MutableLiveData<Long> (it contain an id in the LiveData<List<Item>>)
The MyViewModel.kt code is look like this
class MyViewModel(val repository: Repository) : ViewModel() {
..........
......
val selectionId: MutableLiveData<Long> by lazy {
MutableLiveData<Long>()
}
fun setSelectionId(id: Long) {
selectionId.postValue(id)
}
..........
......
val itemLiveList: LiveData<List<Item>> = liveData(Dispatchers.IO) {
emitSource(repository.getItems())
}
}
If it is an List<Item> i can do somethig like this
val ItemWithSelection: List<Item> = repository.getItems().apply {
this.forEach {
if (it.id == selectionId) {
it.selection = true
}
}
}
but i don't know how to achieve this using Mediator LiveData . Please help me
I don't understand everything in your code, for example I have never seen a function called liveData(CoroutineDispatcher). But do you mean you want something like this?
val listWithoutSelection = liveData(Dispatchers.IO) {
emitSource(repository.getItems())
}
val listWithSelection = MediatorLiveData<List<Item>>().apply {
addSource(listWithoutSelection) { updateListSelection() }
addSource(selectionId) { updateListSelection() }
}
fun updateListSelection() {
listWithSelection.value = listWithoutSelection.value?.map {
if (it.id == selectionId.value)
it.copyWithSelection(true)
else
it
}
}
The copyWithSelection could be easily done with Kotlin data classes. It is not needed dependent on whether you want to modify the object you get from the database. If you only use that object here, you could just always reset the selection of the others to false and then you can keep the object and you don't need a copy.
I'm trying to write text to a textfield, clear the text and the write a new text. I can't get rid of the old text. The new is written on the old so I see them both. I'm using choosefile and trying to show the selected file in a textfield so I can confirm the selection.
class TestView : View("My View") {
var tf: TextField by singleAssign()
override val root = BorderPane()
init {
with(root) {
center = form {
fieldset("Main") {
field("File") {
vbox {
tf = textfield()
tf.text("678")
tf.clear()
tf.text("999")
}
}
}
}
}
}
}
I expected to to see '999' in the textfield but I see both 678 and 999 at the same place.
You are actually calling the text() builder on the textfield, so you're basically creating two text elements inside the text field. I think you should take a step back and read the guide before you go further :) Here is a cleaned up version of your code:
class TestView : View("My View") {
val filename = SimpleStringProperty()
override val root = borderpane {
center {
form {
fieldset("Main") {
field("File") {
textfield(filename)
button("Browse...").action {
val filters = arrayOf(FileChooser.ExtensionFilter("All Files", "*.*"))
chooseFile("Choose file...", filters).firstOrNull()?.let {
filename.value = it.path
}
}
}
}
}
}
}
}
The TornadoFX docs describe using the ListCellFragment to bind each cell in a list control to each item model in a list. I am wondering how to do something similar in a flowpane. I'd like to use that kind of class to render a bunch of controls and an SVG drawing in each cell. (So it would replace the button component in the example code below and somehow bind the shapeItem model to it).
class LibraryView : View("Current Library") {
val shapeLibraryViewModel : LibraryViewModel by inject()
override val root = anchorpane{
flowpane {
bindChildren(shapeLibraryViewModel.libraryItemsProperty){
shapeItem -> button(shapeItem.nameProperty)
}
}
}
}
Since I don't see a pre-made class like the one for list view, perhaps I would need to create something similar to it...or maybe there's a more lightweight approach?
Using an ItemFragment is a bit overkill, since the itemProperty of the fragment will never change (a new fragment would be created for every item whenever the libraryItemsProperty change. However, if your view logic for each item is substantial, this approach will provide a clean way to separate and contain that logic, so it might be worth it. Here is a complete example you can use as a starting point.
class ShapeItemFragment : ItemFragment<ShapeItem>() {
val shapeModel = ShapeItemModel().bindTo(this)
override val root = stackpane {
label(shapeModel.name)
}
}
class ShapeItem(name: String) {
val nameProperty = SimpleStringProperty(name)
}
class ShapeItemModel : ItemViewModel<ShapeItem>() {
val name = bind(ShapeItem::nameProperty)
}
class LibraryViewModel : ViewModel() {
val libraryItemsProperty = SimpleListProperty<ShapeItem>(
listOf(
ShapeItem("Shape 1"),
ShapeItem("Shape 2")
).observable()
)
}
class LibraryView : View("Current Library") {
val shapeLibraryViewModel: LibraryViewModel by inject()
override val root = anchorpane {
flowpane {
bindChildren(shapeLibraryViewModel.libraryItemsProperty) { shapeItem ->
val itemFragment = find<ShapeItemFragment>()
itemFragment.itemProperty.value = shapeItem
itemFragment.root
}
}
}
}
A slightly lighter version would be to pass the parameter into your fragment manually and just extend Fragment:
class ShapeItemFragment : Fragment() {
val item: ShapeItem by param()
override val root = stackpane {
label(item.nameProperty)
}
}
You can still bind to changes to properties inside the ShapeItem, since the underlying item won't change (as seen from the ItemFragment) anyway.
Your bindChildren statement would then look like this:
bindChildren(shapeLibraryViewModel.libraryItemsProperty) { shapeItem ->
find<ShapeItemFragment>(ShapeItemFragment::item to shapeItem).root
}