How can the properties of a top app bar be accessed and reused several times? Are there any limitations towards this approach? I want to be able to change the text in a top app bar when necessary.
class MainActivity : ComponentActivity() {
private val topBarTitle: String = "?"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AirlinesTheme {
setContent {
Scaffold(
topBar = { TopBar() },
content = {}
)
}
}
}
}
#Composable
fun TopBar() {
SmallTopAppBar(title = { Text(text = topBarTitle) })
}
}
Usually the important data is stored inside a ViewModel
class MyVM : ViewModel(){
var topBarProperty by mutableStateOf("Initial")
}
Then initialize the VM like this
val vm by viewModels<MyVM>()
Now, use the property in your top-bar
Scaffold(
topBar = { Text(text = vm.topBarProperty) },
content = {}
)
Now, you could do something like vm.topBarProperty = "Updated", from anywhere in your activity, and it will update the value on the topBar. This is because we are using a MutableState<T> type variable which will trigger recompositions on the Composables reading it, when modified.
Related
I have been using StateFlow + sealed interfaces to represent the various UI states in my Android app. In my ViewModel I have a sealed interface UiState that does this, and the various states are exposed as a StateFlow:
sealed interface UiState {
class LocationFound(val location: CurrentLocation) : UiState
object Loading : UiState
// ...
class Error(val message: String?) : UiState
}
#HiltViewModel
class MyViewModel #Inject constructor(private val getLocationUseCase: GetLocationUseCase): ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
// ...
}
Then in a Composable, I observe the events in this manner:
#Composable
fun MyScreen(
viewModel: HomeScreenViewModel,
onLocationFound: (CurrentLocation) -> Unit,
onSnackbarButtonClick: () -> Unit
) {
// ...
LaunchedEffect(true) { viewModel.getLocation() }
when (val state = viewModel.uiState.collectAsState().value) {
is UiState.LocationFound -> {
Log.d(TAG, "MyScreen: LocationFound")
onLocationFound.invoke(state.location)
}
UiState.Loading -> LoadingScreen
// ...
}
}
In my MainActivity.kt, when onLocationFound callback is invoked, I am supposed to navigate to another destination (Screen2) in the NavGraph:
enum class Screens {
Screen1,
Screen2,
// ...
}
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MyTheme {
MyNavHost(navController = navController)
}
}
}
}
#Composable
fun MyNavHost(navController: NavHostController) {
val context = LocalContext.current
NavHost(navController = navController, startDestination = Screens.Home.name) {
composable(Screens.Screen1.name) {
val viewModel = hiltViewModel<MyViewModel>()
MyScreen(viewModel = viewModel, onLocationFound = {
navController.navigate(
"${Screens.Screen2.name}/${it.locationName}/${it.latitude}/${it.longitude}"
)
}, onSnackbarButtonClick = { // ... }
)
}
// ....
composable("${Screens.Screen2.name}/{location}/{latitude}/{longitude}", arguments = listOf(
navArgument("location") { type = NavType.StringType },
navArgument("latitude") { type = NavType.StringType },
navArgument("longitude") { type = NavType.StringType }
)) {
// ...
}
}
}
But what happens is that the onLocationFound callback seems to be hit multiple times as I can see the logging that I've placed show up multiple times in Logcat, thus I navigate to the same location multiple times resulting in an annoying flickering screen. I checked that in MyViewmodel, I am definitely not setting _uiState.value = LocationFound multiple times. Curiously enough, when I wrap the invocation of the callback with LaunchedEffect(true), LocationFound gets called only two times, which is still weird but at least there's no flicker.
But still, LocationFound should only get called once. I have a feeling that recomposition or some caveat with Compose navigation is in play here but I've researched and can't find the right terminology to look for.
I'm trying to implement One-Tap together with Jetpack Compose. The startDestination in my NavGraph is a screen called AuthScreen. Inside the ativity I have defined a launcher like this:
class MainActivity : AppCompatActivity() {
private lateinit var resultLauncher: ActivityResultLauncher<IntentSenderRequest>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
resultLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
if (result.resultCode == RESULT_OK) {
//Do stuf
}
}
}
}
And inside the AuthScreen I try to sign-in like this:
fun AuthScreen(
viewModel: AuthViewModel = hiltViewModel()
) {
Scaffold(
topBar = {
DarkTopBar()
}
) {
Box(
modifier = Modifier.fillMaxSize().padding(bottom = 48.dp)
) {
Button(
onClick = {
viewModel.oneTapSignIn()
}
) {
Text(
text = "Sign-in"
)
}
}
}
when(val response = viewModel.oneTapSignInState.value) {
is Response.Success -> {
val beginSignInResult = response.data
val intent = IntentSenderRequest.Builder(beginSignInResult.pendingIntent.intentSender).build()
//resultLauncher.launch(intent) 👈 Here is the problem.
}
is Response.Error -> print(response.error)
}
}
But I cannot use resultLauncher since it's placed in the activity and not inside the composable function. Both exist in different files. How to call resultLauncher that exists in the activity from a composable function?
Or is there a better way? For example, to move the resultLauncher inside the composable function? Any ideas will highly appreciate it. Thanks
I have a home screen in my application that is basically content with a navigation bar
Each of the three selections of the navigation bar lead to a different screen, so the code looks like this:
#Composable
fun HomeScreen(state: HomeState, event: (HomeEvent) -> Unit) {
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
BottomNavigation { .... //add the three bottom navigation menu items
}
},
) {
NavHost(
navController = navController,
startDestination = "news",
) {
composable(route = "news") {
val newsVm: NewsViewModel = hiltViewModel()
NewsScreen(newsVm)
}
composable(route = "tickets") { NewTicketScreen() }
composable(route = "archive") { ArchiveScreen() }
}
}
}
this works correctly
this homescreen is used by the following composeable to actually draw the screen
#Composable
fun HomeScreen(
vm: HomeViewModel = hiltViewModel()
) {
val state = vm.state.value
HomeScreen(state, vm::process )
}
so HomeScreen has its own viewmodel
in this example let us take the NewsScreen which takes as an argument its own viewmodel
What this viewmodel will do is load news articles and show them to the user. But in order to not have to reload data every time the user changes the shown screen, what I would do before compose, is pass the homeViewModel as an argument to the newsViewModel.
Home would contain the data loaded up to now and expose it to its children.
and news would load data and save the loaded data in homeViewmodel
so it would go something like this
class HomeViewModel()..... {
internal val newsArticles = mutableListOf()
}
class NewsViewModel() ..... {
val parent :HomeViewModel = ????
val list = mutableStateOf<List<NewsArticle>>(listOf())
init {
val loaded = parent.newsArticles
loadData(loaded)
}
fun loadData(loaded :List<NewsArticle>) {
if (loaded.isEmpty()) {
list.value = repo.loadNews()
} else {
list.value = loaded
}
}
}
I know that I could do the above in my repository, and have it do the caching, but I also use the homeViewModel for communication between the screens , and if the user has to log in , the app uses the MainActivity's navController to start a new screen where the user will log in.
Is there a way to have a reference to the parent viewmodel from one of the children?
You can either explicitly call the viewmodel that you want to contact by injecting both viewmodel belonging to same nav graph.
Alternatively, you can share a interface among both viewmodels, ensure it is same instance and use it as communication bridge.
interface ViewModelsComBridge<T>{
fun registerCallback(onMessageReceived : (T) -> Unit)
fun onDispatchMessage(message : T)
fun unregister(onMessageReceived : (T) -> Unit)
}
and in your view models:
class ViewModelA #Inject constructor(private val bridge : ViewModelCommunicationBridge<MyData>, ...) : ViewModel(){
init {
bridge.registerCallback { //TODO something with call }
}
}
in second view model:
class ViewModelA #Inject constructor(private val bridge : ViewModelCommunicationBridge<MyData>, ...) : ViewModel(){
fun onClick(){
val myData = processMyData()
bridge.onDispatchMessage(myData)
}
}
On the other end the other viewmodel will receive this call if it is alive.
Ensure your implementation is inject correctly and it is same instance in both viewmodels.
Your can change your NewsViewModel 's viewModelStoreOwner(fragment, activity or HomeScreen's destination), not the lifecycle of news's destination.
so your data will be survive while NewsScreen changes.
#Composable
fun HomeScreen(state: HomeState, event: (HomeEvent) -> Unit) {
val navController = rememberNavController()
val newsVm: NewsViewModel = hiltViewModel() //move to here,
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
BottomNavigation { .... //add the three bottom navigation menu items
}
},
) {
NavHost(
navController = navController,
startDestination = "news",
) {
composable(route = "news") {
NewsScreen(newsVm)
}
composable(route = "tickets") { NewTicketScreen() }
composable(route = "archive") { ArchiveScreen() }
}
}
}
I am trying to use ViewModel with Jetpack Compose,
By doing a number increment.
But it's not working. Maybe I'm not using the view model in right way.
Heres my Main Activity code
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting()
}
}
}
#Composable
fun Greeting(
helloViewModel: ViewModel = viewModel()
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(
text = helloViewModel.number.toString(),
fontSize = 60.sp,
fontWeight = FontWeight.Bold
)
Button(onClick = { helloViewModel.addNumber() }) {
Text(text = "Increment Number ${helloViewModel.number}")
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
JetpackcomposepracticeTheme {
Greeting()
}
}
And here is my Viewmodel class.
It works fine with xml.
How do i create the object of view model:
class ViewModel: ViewModel() {
var number : Int = 0
fun addNumber(){
number++
}
}
Compose can recompose when some with mutable state value container changes. You can create it manually with mutableStateOf(), mutableStateListOf(), etc, or by wrapping Flow/LiveData.
class ViewModel: ViewModel() {
var number : Int by mutableStateOf(0)
private set
fun addNumber(){
number++
}
}
I suggest you start with state in compose documentation, including this youtube video which explains the basic principles.
I use Hilt along with view-Model. Here is another way of using it
#Composable
fun PokemonListScreen(
navController: NavController
) {
val viewModel = hiltViewModel<PokemonListVm>()
val lazyPokemonItems: LazyPagingItems<PokedexListEntry> = viewModel.pokemonList.collectAsLazyPagingItems()
}
I use this composable instead of a fragment and I keep one reference to the composable in the parent
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()")
}
}