I'm having issues whit the onClick on Jetpack compose, it performs the click as soon as I run the app and after returning to this activity the button stops working. Any insights?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val context = LocalContext.current
linkButton("Go to text views", goTo(context, TextViewActivity::class.java))
}
}
}
#Composable
fun linkButton(msg: String, link: Unit) {
Button(onClick = {
link
}) {
Text(msg)
}
}
#Preview
#Composable
fun PreviewMessageCard() {
val context = LocalContext.current
linkButton(
msg = "Sample",
link = goTo(context, TextViewActivity::class.java)
)
}
private fun goTo(context: Context, clazz: Class<*>) {
context.startActivity(Intent(context, clazz))
}
You are actually calling the method at the moment you are composing the linkButton, not passing it as a callback to be called on click. And on click, it is just returning a Unit which causes the unexpected behavior.
To fix that, you should change the parameter type in your composable function to () -> Unit, which represents a function type.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val context = LocalContext.current
linkButton("Go to text views") {
goTo(context, TextViewActivity::class.java)
}
}
}
}
#Composable
fun LinkButton(msg: String, link: () -> Unit) {
Button(onClick = {
link()
}) {
Text(msg)
}
}
Related
I tried to implement a timer in a ViewModel that after ten seconds returns to the previous view, but it doesn't work at all. I wonder if CountDownTimer can even be used in a ViewModel.
class PairingScreenViewModel(
private val routing: NavController,
) : ViewModel() {
var content = mutableStateOf(10)
var counter: CountDownTimer? = null
init {
viewModelScope.launch {
delay(1000L)
counter = object : CountDownTimer(9000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
content.value = content.value.dec()
}
override fun onFinish() {
routing.popBackStack() // Go back twice. WHY?
}
}.start()
}
}
override fun onCleared() {
super.onCleared() // It seems that it never runs.
counter?.cancel()
}
fun onButtonClicked() = viewModelScope.launch {
counter?.cancel() // It doesn't cancel the timer.
routing.navigate("next-route")
}
}
EDIT: This is my MainActivity.kt, I am probably doing something wrong with NavHostController.
This is the first time I use androidx.navigation:navigation-compose.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val routing = rememberNavController()
val heading = "MyApplication"
AndroidNavigationDemoTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(heading, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) }
)
},
content = {
Router(routing)
}
)
}
}
}
}
}
#Composable
fun Router(routing: NavHostController) {
NavHost(
navController = routing,
startDestination = "welcome"
) {
composable("welcome") {
WelcomeScreen(routing)
}
composable("hisense") {
HisenseScreen(HisenseScreenViewModel(routing))
}
composable("pairing") {
PairingScreen(PairingScreenViewModel(routing))
}
// ...
}
}
I have to remove ViewModel from the NavHost.
#Composable
fun Router(routing: NavHostController) {
NavHost(
navController = routing,
startDestination = "welcome"
) {
composable("welcome") {
WelcomeScreen(routing)
}
// ...
composable("pairing") {
PairingScreen(routing)
}
// ..
}
}
And set the ViewModel from the composable.
class PairingScreenViewModelFactory(private val routing: NavController) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T = PairingScreenViewModel(routing) as T
}
#Composable
fun PairingScreen(routing: NavController) {
val viewModel: PairingScreenViewModel = viewModel(factory = PairingScreenViewModelFactory(routing))
// ...
}
And everything is working like a charm.
I have read the Android official artical.
I see that MutableStateFlow is hot Flow and is observed by Compose to trigger recomposition when they change.
The Code A is from the the Android official artical, it's OK.
I'm very stranger why the author need to invoke collect to get latest value for Compose UI in Code A.
I think the Compose UI can always get the latest value of latestNewsViewModel.uiState, why can't I use Code B do the the same work?
Code A
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
latestNewsViewModel.uiState.collect { uiState ->
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
Code B
class LatestNewsActivity : ComponentActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SoundMeterTheme {
Surface(color = MaterialTheme.colors.background) {
Greeting(latestNewsViewModel)
}
}
}
}
}
#Composable
fun Greeting(latestNewsViewModel: LatestNewsViewModel) {
val myUIState by remember{ latestNewsViewModel.uiState }
when (myUIState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
//The same
Add Content
To RaBaKa 78: Thanks!
By your opinion, can I use Code C instead of Code A?
Code C
class LatestNewsActivity : ComponentActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SoundMeterTheme {
Surface(color = MaterialTheme.colors.background) {
Greeting(latestNewsViewModel)
}
}
}
}
}
#Composable
fun Greeting(latestNewsViewModel: LatestNewsViewModel) {
val myUIState by remember{ latestNewsViewModel.uiState.collectAsState() }
when (myUIState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
//The same
Compose need State not StateFlow to recompose accordingly,
you can easily convert StateFlow to State in compose
val myUiState = latestNewsViewModel.uiState.collectAsState()
There is no need of using a remember {} because your StateFlow is from your viewModel, so it can manage the recomposition without remember
So like CODE B you can manually check the state of the StateFLow or convert to State and automatically recompose when the state changes.
The Code A is XML way of doing things where you can call other functions but in Compose you should do that steps in your viewModel
CODE D
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
#Composable
fun Greeting(latestNewsViewModel: LatestNewsViewModel) {
val myUIState = latestNewsViewModel.uiState.collectAsState()
Column(modifier = Modifier.fillMaxSIze()) {
when(myUIState) {
is LatestNewsUiState.Success -> SuccessComposable(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception) -> ErrorComposable(uiState.exception)
}
}
}
I cannot collect any state from the same Jetpack Compose screen(JCScreen), after having it wrapped in a NavHost.
The original working solution:
this JcScreen is for user sign in
the activity ActAuth
#AndroidEntryPoint
class ActAuth : AppCompatActivity() {
private val viewModel: ViewModelAuth by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ThemeBasicHRLQ {
ScreenSignInWithInputValidationDebounce()
}
}
// to collect the state `stateSignIn`,
// and it works fine
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateAuth.collect { state ->
...
}
}
}
}
}
the digest of the JCScreen ScreenSignInWithInputValidationDebounce():
#OptIn(ExperimentalComposeUiApi::class, kInternalCoroutinesApi::class)
#Composable
fun ScreenSignInWithInputValidationDebounce(
viewModel: ViewModelAuth = hiltViewModel()
){
....
val stateSignIn by viewModel.stateAuth.collectAsState()
...
}
Everything works fine until I consolidating highly similar JCScreen for user sign up.
#AndroidEntryPoint
class ActAuth : AppCompatActivity() {
private val viewModel: ViewModelAuth by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ThemeBasicHRLQ {
// the new JCScreen
ScreenAuth()
}
}
// to collect the state `stateSignIn`
// but, with the new JCScreen, it does not work any more
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stateAuth.collect { state ->
...
}
}
}
}
}
#Composable
fun ScreenAuth() {
val navController = rememberNavController()
NavHostAuth(navController = navController)
}
#Composable
fun NavHostAuth(
navController: NavHostController,
) {
NavHost(
navController = navController,
startDestination = NavRoutesAuth.SignIn.route
) {
// I am still in this same JCScreen,
// without using the `navController`
composable(NavRoutesAuth.SignIn.route) {
ScreenSignInWithInputValidationDebounce(navController)
}
composable(NavRoutesAuth.SignUp.route) {
ScreenSignUpWithInputValidationDebounce(navController)
}
}
}
enum class NavRoutesAuth(val route: String) {
SignIn("auth/signin"),
SignUp("auth/signup"),
}
It looks to me that once wrapped into a NavHost, the state management is different.
Here may be a similar problem, with which, however, I still cannot solve my problem above.
I wanted to build a very simple demo. A button which you can click, and it counts the clicks.
Code looks like this:
class MainActivity : ComponentActivity() {
private var clicks = mutableStateOf(0)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface(color = MaterialTheme.colors.background) {
NewsStory(clicks.value) { onClick() }
}
}
}
private fun onClick() {
clicks.value++
}
}
#Composable
fun NewsStory(clicks: Int, onClick: () -> Unit) {
Column(modifier = Modifier.padding(8.dp)) {
Button(onClick = onClick) {
Text("Clicked: $clicks")
}
}
}
From my understanding this should be recomposed everytime the button is clicked, as clicks is changed.
But it does not work, any ideas what I'm doing wrong here?
I'm on androidx.activity:activity-compose:1.3.0-beta01, kotlin 1.5.10 and compose version 1.0.0-beta08
You need to use the "remember" keyword for the recomposition to happen each time, as explained here: https://foso.github.io/Jetpack-Compose-Playground/general/state/
In short, your composable would look like this:
#Composable
fun NewsStory (){
val clickState = remember { mutableStateOf(0) }
Column (modifier = Modifier.padding(8.dp)) {
Button(
onClick = { clickState.value++ }) {
}
Text("Clicked: $clickState.value.toString()")
}
}
I have tried to basic composable codelab exercise. In Android Studio BasicCodelabThemes shows as an error. Please help me to find the error
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Greeting("Android")
}
}
}
}
#Composable
fun MyApp(content:#Composable () -> Unit) {
BasicsCodelabTheme {
Surface(color = Color.Yellow) {
content()
}
}
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
Yeah from my research on the BasicCodelabTheme, it is a custom-built composable function, it isn't a predefined one, so you need to create it yourself in your Kotlin file as a composable function for your theme.BasicCodelabTheme function definition