Display a random layout in kotlin - android

I am trying to create an app that display random challenges in different levels of difficulty to build a gamified self-development app. As i am a pretty unexperienced developer(this is my first app that is not part of any course), I didn't used fragments but i actually created a layout that correspond to every challenge. In my app, i am displaying as MainActivity the different levels of difficulty which are represented by buttons. Each of those level button create an intent to an introductory page to the level which also contains a button at the bottom that should randomly select one of the layouts(categorized as part of this level) and display it to the user. My problem is that i don't know the code to do this kind of selection and previous answers didn't worked very well.
I tried to do this based on the answers:
private val SafeChallenges = listOf(
DeclutterPhone::class,
Drink2glasses::class,
TodoList::class
)
private fun startRandomActivity() {
startActivity(Intent(this, SafeChallenges.random().java))
// If this s in a Fragment, use requireContext() instead of this
}
But it didn't worked out. I created 3 classes to have each one with a setContentView to a specific layout(challenge) I know, there is no setOnClickListener but when i put one it either show me an error or doesn't do anything.
I also tried this type of loop but my button doesn't respond to it.
override fun onClick(view: View){
Log.d(TAG,"onclick: called")
category12_challenge_button.setOnClickListener {
Log.d(TAG,"button clicked")
val myRandomChallenges = Random.nextInt(1..3)
when(myRandomChallenges){
1->startActivity(Intent(this,DeclutterPhone::class.java))
2->startActivity(Intent(this,Drink2glasses::class.java))
3->startActivity(Intent(this,TodoList::class.java))
else -> IllegalArgumentException("unknown layout")
}
}
Could you tell me where I missed my point?

For android navigation try this:
val possibleDestinations = listOf(
R.id.action_mainFragment_to_firstFragment,
R.id.action_mainFragment_to_secondFragment,
R.id.action_mainFragment_to_thirdFragment)
viewBinding.apply {
randPick.setOnClickListener {
Navigation.findNavController(view).navigate(possibleDestinations.random())
}
}
To see it in action visit this working example on GitHub

Here is the definitive answer to the randomization loop of my algorithm. My main problem was that i didn't initialized the setOnclicklistener in onCreate when i made my onClick function.
I based my code on Tenfour04 answer even if his/her answer seems to be gone.
Anyways, thank you guys. I promise, next question will be much more clean and detailed ;)
There is the code for those that might want it, i created 3 classed to have 3 setContentView for 3 different XML layouts:
override fun onClick(view: View) {
val safeChallenges = listOf(
DeclutterPhone::class,
Drink2glasses::class,
TodoList::class
)
val intent = when(view.id){
R.id.category12_challenge_button ->{
Intent(this, safeChallenges.random().java)
}
else -> throw IllegalArgumentException("unknown layout selected")
}
startActivity(intent)
}

Related

LazyColumn is showing wrong display when deleting an item

I'm creating an app that, among other things, enables the user to add a Consumer, and then remove him later. The consumers are shown in cards with a remove button in the end.
Adding a consumer works fine. However, when I try to remove a consumer, the one removed in the app screen is always the last one. I know this is not a logic implementation mistake, because I stopped the Debugger right before the items() call, and in any recomposition the list holding the consumers has the correct consumer removed! The following image shows the result after clicking the Remove button from the "B" card (the card removed is "C"!):
Look what the debugger shows right before the recomposition takes place:
The relevant code is below.
The ViewModel and Model (relevant part) definitions:
class ConsumidoresViewModel : ViewModel() {
var lista = mutableStateListOf<Consumidor>()
fun add(consumidor: Consumidor){
lista += consumidor
}
fun remove(consumidor: Consumidor){
lista.remove(consumidor)
}
}
data class Consumidor(var nome: String)
...
The main composable, called directly from .onCreate():
fun UsersView() {
var consumidores: ConsumidoresViewModel = viewModel()
...
LazyColumn() {
items(items = consumidores.lista) { consumidor ->
CardNome(consumidor, consumidores)
}
}
The fucntion call of the Remove button:
IconButton(onClick = { consumidorViewModel.remove(consumidor) }) { ... }
I can't figure out what I'm doing wrong. I'm fairily new with Android Programming / Compose, but I have been programming for decades (not professionaly). Can someone point me to a direction? It probably has something to do with my Sates / View Model implementation, but I can't find out what, as the SnapshotStateList on the debugger clearly shows "A" and "C" cards present, and "B" gone!
Based on the official docs.
By default, each item's state is keyed against the position of the item in the list or grid. However, this can cause issues if the data set changes, since items which change position effectively lose any remembered state. If you imagine the scenario of LazyRow within a LazyColumn, if the row changes item position, the user would then lose their scroll position within the row.
So it's usually a good set up when your data class has a unique property like an id if you plan to manipulate a collection of it (like your removal operation), you can then use it as a key = {...} for the LazyColumn so it knows not to use the index as a unique identifier for its item elements, and that could be the reason why your'e having a wrong display of items after removing an element from the list.
LazyColumn() {
items(items = consumidorList, key = { it.id }) { consumidorItem ->
...
}
}
Update:
Linking my another answer for a movableContentOf{...} sample.

type mismatch when trying to read List from internal Storage

I am new to Kotlin so please excuse this question, as it is probably pretty stupid...
So I followed the tutorial by Philipp Lackner to create a Todo-List as an android app if any of you know that. Now I tried to add a read and write functionality to this app by saving into a simple .txt-file for now.
For that I tried to follow this tutorial as much as possible, but now I am running into Problems when writing code to load the Todo-Items from a file.
I wrote this function to load Todo-Items from the .txt-file:
private suspend fun loadTodoItemsFromInternalStorage(): List<Todo> {
return withContext(Dispatchers.IO) {
val todoItemList: MutableList<Todo> = mutableListOf<Todo>()
var isEven = true
val files = filesDir.listFiles()
files?.filter { it.canRead() && it.isFile && it.name.endsWith(".txt") }?.map {
val lines = it.bufferedReader().readLines()
for (i in lines.indices) {
isEven = if(isEven) {
todoItemList.add(Todo(lines[i], lines[i+1].toBoolean()))
!isEven
} else {
!isEven
}
}
todoItemList
} ?: mutableListOf<Todo>()
}
}
Why do I get that type mismatch? I even initialize the list I want to return as a MutableList of type Todo, but I guess the type inference of Kotlin turns it into a MutableList of type Any?
So how do I fix this? And if you want to you could tell me better ways to do what I did (e.g. saving Todo-items (which consist of title and a boolean whether they are checked or not) to a file)
My plan to keep this as simple as possible as this is my first Kotlin project was to just use 2 lines for a Todo-item, where the first line is the title and the second line is the status whether it has been checked or not. I hope that makes my code easier to understand.
Thank you so much for your help in advance! I appreciate it a lot, as I have been struggling with coding in the past and really want to improve my coding skills.
If you cast your return statement, the code should work. Taking your code as a basis:
private suspend fun loadTodoItemsFromInternalStorage(): List<Todo> {
return withContext(Dispatchers.IO) {
...
} ?: mutableListOf<Todo>()
} as MutableList<Todo>
}
Some additional suggestions:
when dealing with file handling in general: error handling, check for correct format, empty entries, ...
the check for isEven/isOdd is obsolete and the code can be shortened to great extent when you use the step-option within the for-loop
for (i in lines.indices step 2) {
todoItemList.add(Todo(lines[i], lines[i+1].toBoolean()))
}

Starting Activity using Compose Navigation

I'm trying to launch an Activity by clicking on a button set on a BottomNavBar. There's a Compose Navigation set up using NavGraphBuilder.navigation() with a composable() call for each Compose screen like:
navigation(
startDestination = "home",
route = "main"
) {
composable("home") {
HomeScreen(...)
}
// Several more Screens
}
I've found out about NavGraphBuilder.activity(), so I was thinking something like:
activity("tickets") {
this.activityClass = ExternalActivity::class
}
And it works, if ExternalActivity doens't need any data to be passed to it. But it does.
The only viable alternative that comes to mind is using a composable() and launching the activity from there:
composable("tickets") { backStackEntry ->
val config = // get config from arguments
context.startActivity(
Intent(context, ExternalActivity::class.java).apply {
putExtra("config", config)
}
)
}
Or something along those lines. But it's kind of messy and has some side effects, so I'd like to avoid it.
Is there any way to use the activity() call and pass data to the Activity being launched?
I'm limited by the architecture of the codebase in which I'm working, so yes, it needs to be an Activity (it's actually from an external library).
Thanks.
The only thing that remotely resembles what you are trying to do would be data.
activity("tickets") {
this.activityClass = ExternalActivity::class
this.data = "Hello World".toUri() <--
}
[ExternalActivity]
override fun onCreate(savedInstanceState: Bundle?) {
...
val data = intent.data
But data is Uri, so it might not suit your needs, especially if you are dealing with an external library. Then context.startActivity() would be the next choice, as in your second approach.
One thing to note is that when you use context.startActivity() (instead of NavGraphBuilder.activity()), you need to set the "current" destination correctly when the Activity closes (e.g. call navController.navigateUp() or navController.popBackStack()). If not, it will jump back to the Activity you just closed, because as far as NavController is concerned, the Activity you started (and now closed) is the current destination.

Kotlin: How to rid of function overload boilerplate?

I am working with overload functions in Kotlin.
In this schematic example, suppose a function whose only difference is the type of view that I pass to the function. One uses TextView, the other uses Button, so I have 2 different functions.
fun workWithViews(v:TextView,...){
// code
}
fun workWithViews(v:Button,...){
// same code!
}
In this case, the properties I use are the same (isAllCaps, gravity, etc.). The problem is that I have to place the same code twice, i.e., the whole code is exactly the same.
It happens because isAllCaps (just like many other properties) it is not a general property of a view, but of some types of views
So it doesn't work, because obviously the compiler see the function parameter, not the real parameter.
I also can make function with a view type parameter, with a big when with my type possibilities:
fun workWithTextView(v:View,...){
when{
(v is TextView) -> {
// code
}
(v is Button) {
// same code
}
} // when
}
The 2 solutions are terrible and generate duplicate code or boilerplate.
I can also do the when before each access to some field, which makes things even worse. Now imagine if one has 5 similar types instead of 2, with many fields in common.
I read some suggestions to create union types in Kotlin. It would be great!
For instance:
fun workWithViews(v:(TextView, Button),...){
// just one code repetition....
}
or
union textBut = TextView , Button
fun workWithViews(v:textBut ,...){
// just one code repetition....
}
In that case I would only have to test a certain type (if (v is typeX)) if I used something specific for that type.
Is there some best solution?
Button is a subclass of TextView, so you can make the function signature take a TextView and put Button-specific stuff in an if-block.
fun workWithTextView(textView: TextView) {
// Do stuff common to TextViews and Buttons.
if (textView is Button) {
// Do extra stuff only for Buttons.
}
}
If Views have common methods, then most likely one view extends the other.
For example, Button extends TextView and you can do this:
fun workWithViews(v:TextView){
// TextView code
}
fun workWithViews(v:Button){
// Button specific code
workWithViews(v as TextView)
}

How can I create dynamic/conditional navigation with Jetpack Navigation?

I've come across an interesting problem with trying to accomplish dynamic or conditional navigation with the Jetpack Navigation library.
The goal I have in mind is to be able to continue using the nav_graph.xml to manage the overall navigation graph, but simultaneously allow for conditional navigation based on some factors.
I have included some code below that shows where my solution is headed. The problem is that it inherently requires a lot of maintenance for future conditional logic to work.
I really want the navigateToDashboard function in the example to be able to be executed with either no parameters, or parameters that rarely change. For instance, instead of passing NavDirections, maybe passing some identifier that let's the navigateToDashboard function know which NavDirections to return.
Code for the class managing the conditional logic.
class DynamicNavImpl(private val featureFlagService: FeatureFlagService) : DynamicNav {
override fun navigateToDashboard(navDirectionsMap: Map<Int, NavDirections>): NavDirections {
val destinationIdRes = if (featureFlagService.isDashboardV2Enabled()) {
R.id.dashboardV2Fragment
} else {
R.id.dashboardFragment
}
return navDirectionsMap[destinationIdRes] ?: handleNavDirectionsException(destinationIdRes)
}
private fun handleNavDirectionsException(destinationIdRes: Int): Nothing {
throw IllegalStateException("Destination $destinationIdRes does not have an accompanying set of NavDirections. Are you sure you added NavDirections for it?")
}
}
Call site examples
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboard()),
Pair(R.id.dashboardV2Fragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboardV2())
)
)
)
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, EmailLoginFragmentDirections.emailLoginToDashboard()),
Pair(R.id.dashboardV2Fragment, EmailLoginFragmentDirections.emailLoginToDashboardV2())
)
)
)
Looking at the call site, you could see how this could be problematic. If I ever want to add a new potential destination, let's say dashboardV3Fragment, then I'd have to go to each call site and add another Pair.
This almost defeats the purpose of having the DynamicNavImpl class. So this is where I am stuck. I want to be able to encapsulate the various variables involved in deciding what destination to go to, but it seems with how NavDirections are implemented, I'm not able to.
I went between a few different approaches, and I landed on something that still doesn't feel ideal, but works for my use case.
I completely abandoned the idea of using a central dynamic navigation manager. Instead, I decided on having a "redirect" or "container" Fragment that decides what Fragment to show.
So here's the new code inside of the DashboardRedirectFragment
childFragmentManager.beginTransaction().replace(
R.id.dashboard_placeholder,
if (featureFlagService.isDashboardV2Enabled()) {
DashboardV2Fragment.newInstance()
} else {
DashboardFragment.newInstance()
}
).commit()
The way I'm using this is by registering a new destination in my nav graph called dashboardRedirectFragment, and anything in the graph that needs access to the dashboard use the dashboardRedirectFragment destination.
This fully encapsulates the dynamic navigation logic in the redirect Fragment, and allows me to continue using my nav graph as expected.

Categories

Resources