I have horizontal pager as below.
#Composable
fun TabsContent(tabs: List<MyTabItem>, pagerState: PagerState) {
HorizontalPager(state = pagerState, pageCount = tabs.size) { page ->
tabs[page].screen()
}
}
The MyTabItem is as below
typealias ComposableFun = #Composable () -> Unit
sealed class MyTabItem(
var icon_filled: Int,
var icon_unfilled: Int,
var title: String,
var screen: ComposableFun,
) {
object View :
MyTabItem(
R.drawable.ic_view,
R.drawable.ic_view,
"View",
{ ViewScreen() },
)
object Save: MyTabItem(
R.drawable.ic_add_save,
R.drawable.ic_add_save,
"Save",
{ CreateScreen() },
)
}
I want to pass the navContoller to the TabsContent Method, which needs to be accessed via Viewscreen(). How to pass it.
Thanks in advance.
You can use this navContoller.navigate(tabs[page].title /*route path*/), and u should by setup NavHost.
You can take look this doc:
Url
Related
I have a composable function named 'Page' as a basic composable to hold NavHost for my app, please see architecture below:
#Composable
fun Page(viewModel: LdvToolViewModel = hiltViewModel(), scaffoldState: ScaffoldState, navController: NavHostController){
val statusBarMode = viewModel.statusBarUiState
val uiController = rememberSystemUiController()
LaunchedEffect(statusBarMode){
uiController.run {
if(statusBarMode.isDarkContent){
setStatusBarColor(color = Color.White, darkIcons = true)
}else{
setStatusBarColor(color = LdvOrange, darkIcons = false)
}
}
}
val navBuilder: NavGraphBuilder.() -> Unit = {
composable(LdvPages.SEARCHING.name) { SearchUi(viewModel, scaffoldState = scaffoldState) }
composable(LdvPages.ERROR.name) { ErrorUi(viewModel,scaffoldState = scaffoldState) }
composable(LdvPages.PANEL.name) { PanelUi(scaffoldState,viewModel, mBaseViewModel) }
composable(LdvPages.PrivacyPolicy.name){ PrivacyPolicy(scaffoldState)}
composable(LdvPages.TermsOfUse.name){ TermsOfUse(scaffoldState)}
composable(LdvPages.OpenSourceLicense.name){ OpenSourceLicense(scaffoldState)}
composable(LdvPages.DebugPage.name){ DebugPage(viewModel)}
}
val start by derivedStateOf {
if (...){
LdvPages.PANEL.name }else if(...){
LdvPages.ERROR.name
}else{LdvPages.SEARCHING.name}
}
NavHost(navController = navController, startDestination = start, builder = navBuilder)
if(!isNfcEnable){
viewModel.setNfcDisableContent()
ErrorDialog(viewModel = viewModel){
startActivity(Intent(Settings.ACTION_NFC_SETTINGS));
}
}
}
As you can see that 'LdvToolViewModel' has been injected to 'Page' as hiltViewModel. To keep 'LdvToolViewModel' as one instance among lifecycles of nested-composable functions in navBuilder, I have to pass it as parameter to those functions. Is there any better way like I can somehow inject 'LdvToolViewModel' in those functions as hiltViewModel and meanwhile I can still have the injected hiltViewModel as a same instance?
Imagine you have a "HomeGraph", with "Home" as a parent destination, and few destination screens that should share the same ViewModel instance.
First get a NavBackStackEntry, by passing your parent route
val parentEntry: NavBackStackEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
Then get an instance of a ViewModel by passing the parent NavBackStackEntry
val userViewModel = hiltViewModel<HomeViewModel>(parentEntry)
Also, remember that if you navigate to Destination.HomeGraph.route either from nested navigation or from a different graph a new instance of ViewModel will be created, so if you navigate within a single graph, navigate to startDestination e.g Destination.Home.route - this way you will keep the same ViewModel instance.
I don't thing we have a well-defined ViewModel sharing in compose as we had with a view system e.g by activityViewModels(), but keeping ViewModel state in graphs while user is not accessing them is a bad practice.
You can always pass the ViewModel in one of the graph extension function if necessary.
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
navigation(
startDestination = Destination.Home.route,
route = Destination.HomeGraph.route
) {
composable(Destination.Home.route) { navBackStackEntry ->
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
val homeViewModel = hiltViewModel<HomeViewModel>(parentEntry)
HomeRoute(
viewModel = homeViewModel,
onNavigate = { dest ->
navController.navigate(dest.route)
})
}
composable(Destination.Search.route) { navBackStackEntry ->
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
val homeViewModel = hiltViewModel<HomeViewModel>(parentEntry)
UserSupportRoute(
viewModel = userViewModel,
onNavigate = { dest ->
navController.navigate(dest.route) {
popUpTo(Destination.Search.route) {
inclusive = true
}
}
})
}
}
I want to develop a LazyLayout in jetpack compose
#ExperimentalFoundationApi
#Composable
fun LazyLayout(
itemsProvider: LazyLayoutItemsProvider!,
modifier: Modifier! = Modifier,
prefetchState: LazyLayoutPrefetchState? = null,
measurePolicy: (#ExtensionFunctionType LazyLayoutMeasureScope.(Constraints) ->
MeasureResult)?
): Unit
there is two necessary parameters, itemsProvider and measurePolicy and this is all information about itemsProvider parameter in document:
#param itemsProvider provides all the needed info about the items which could be used to compose and measure items as part of [measurePolicy].
I don't know how to provide this parameter for LazyLayout.
any idea how it works?
You have to provide only "itemsProvider" parameter by creating object like this:
LazyLayout(
itemsProvider = object : LazyLayoutItemsProvider {
override fun getContent(index: Int): #Composable () -> Unit {
return {
//your content
}
}
override val itemsCount: Int
get() = //count content
override fun getKey(index: Int): Any = index
override val keyToIndexMap: Map<Any, Int> = emptyMap()
override fun getContentType(index: Int): Any? = null
},
//modifier = modifier
//.padding(paddingValues)
//.verticalScroll(state = state, flingBehavior = NoFlingBehavior)
) { constraints ->
//do whatever you want
}
I want to make a TopAppBar switching its content while navigating. The goal is to use a flag and change the navigationIcon. But I can't pass the Composable/null as a parameter here. The code:
val navIcon = if (viewModel.isBackAvailable) NavIcon { navController.navigateUp() } else null
TopAppBar(navigationIcon = navIcon)// Required:(() → Unit)? Found:Unit?
#Composable
private fun NavIcon(navigate: () -> Unit) {
IconButton(onClick = navigate) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = stringResource(R.string.navigate_back),
tint = MaterialTheme.colorScheme.primary
)
}
}
I can't pass something like an empty value navigationIcon = {} because it takes its space in this case, I need to use null.
In your code, navIcon is a result of NavIcon function call, which is unit. You need to have function reference there, you can do that like this:
val navIcon: (#Composable () -> Unit)? = if (viewModel.isBackAvailable) {
{ NavIcon { navController.navigateUp() } }
} else null
I need to pass a compose content parameter in data class. For example a button can render when added into this content.
data class ContentData {
val content: #Composable ()-> Unit
}
This is working but when I get the app background I am getting parcelable exception. How to solve this problem.
One possible explanation I think that will occur related with a parcelable error, happens if you try to pass such object between activities as extras through Intent. Consider not use Composable as parameters in objects. Instead, try to represent the parameters of your Composable with a model which contains the parameters.
// your compose function
#Composable
fun Item(content: String = "Default", padding: Dp){
// ...
}
// Ui Model which contains your data (instead of have a weird composable reference) as a parcelable.
data class ContentData(
val content: String = "Default",
val paddingRaw: Int = 0
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString().orEmpty(),
parcel.readInt()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(content)
parcel.writeInt(paddingRaw)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ContentData> {
override fun createFromParcel(parcel: Parcel): ContentData {
return ContentData(parcel)
}
override fun newArray(size: Int): Array<ContentData?> {
return arrayOfNulls(size)
}
}
}
// Example if you need the model between activities through the intent as an extra.
val data = ContentData("your content", 11)
val intent = Intent().apply {
putExtra("keyContentData", data)
}
//The way of get and use your model.
val contentData = intent.extras?.get("keyContentData") as ContentData
#Composable
fun ParentComponent(){
// ...
Item(
contentData?.content.orEmpty(),
contentData?.paddingRaw?.dp ?: 0.dp
)
// ...
}
I followed the official guide to create viewModel instance and it works perfectly. However, when there is any viewModel in the #composable, Android Studio isn't able to render the preview and with the error code ViewModels creation is not supported in Preview. Anyone got any solution?
P.S. using compose version 1.0.0-alpha06
You could use an approach that looks like this which will show up in the recommended video bellow:
#Composable
fun TestView(
action: MainActions,
viewModel: OnboardViewModel = getViewModel()
) {
TestUI(onClick = viewModel.clickMethod())
}
#Composable
fun TestUI(onClick: () -> Unit) {}
#Preview
#Composable
fun TestUIPreview() {
MaterialTheme() {
TestUI(onClick = {})
}
}
There is a recommendation from google in this video at the selected time: https://youtu.be/0z_dwBGQQWQ?t=573
I had exactly the same problem.
The solution was: Extend the ViewModel with an interface
ComposeView:
#Composable
fun MyScreen(myVm: IMyViewModel = MyViewModel()) {
Text(text = myVm.getTextA())
}
#Preview()
#Composable
fun MyScreenPreview() {
MyScreen(myVm = MyViewModelPreview())
}
ViewModel:
abstract class IMyViewModel : ViewModel(){
abstract val dynamicValue: StateFlow<String>
abstract fun getTextA() : String
}
class MyViewModel : IMyViewModel() {
private val _dynamicValue: MutableStateFlow<String> = MutableStateFlow("")
override val dynamicValue: StateFlow<String> = _dynamicValue
init {
}
override fun getTextA(): String {
return "Details: ${EntityDb.getAllEntities().lastOrNull()?.details}"
}
}
class MyViewModelPreview(override val dynamicValue: StateFlow<String> = MutableStateFlow("no data")) : IMyViewModel() {
override fun getTextA(): String {
return ""
}
}
The #Preview annotated composable class should be agnostic from viewmodel.
i use this solution adding an extra class with models as input
#Composable
fun TabScreen(
viewModel: DetailsViewModel = viewModel()
) {
val details = viewModel.details.observeAsState()
Tabs(
details.value
)
}
#Composable
fun Tabs(
details: Details?
) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
}
#Composable
#Preview(showBackground = true)
fun TabsPreview(
#PreviewParameter(DetailsPreview::class)
details: Details?
) {
AppTheme {
Tabs(details)
}
}
You could use interfaces and hilt.
interface IMyViewModel {
fun getTextA() : String
}
#HiltViewModel
class MyViewModel() : ViewModel(), IMyViewModel {
fun getTextA() : String {
//do some cool stuff
}
}
class MyViewModelPreview() : IMyViewModel {
fun getTextA() : String {
//do some mock stuff
}
}
#Composable
fun MyScreen(myVm = hiltViewModel<MyViewModel>()) {
Text(text = myVm.getTextA())
}
#Preview()
#Composable
fun MyScreenPreview() {
MyScreen(myVm = MyViewModelPreview())
}
In this point MyViewModel is an implementation of IMyViewModel annotated with #HiltViewModel and hiltViewModel make all the required wired for you, In preview you could use any other simple mock implementation.
If you need to provide some dependency to your view model use injected constructor with dagger injection(already supported by hilt). Obviously this dependency should be paced on your actual viewmodel and your preview implementations need to be just a wrapper class with no other dependency since they function is just satisfy arguments
#HiltViewModel
class MyViewModel #Inject constructor(
private val myDependencyRepositoryOne: MyDependencyRepositoryOne,
private val myDependencyRepositoryTwo: MyDependencyRepositoryTwo)
: ViewModel(), IMyViewModel {
fun getTextA() : String {
//do some cool stuff
}
}
Here is another useful resource related to viewmodel injection in compose
what I am using:
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyMVIApp1Theme {
val myViewModel = remember { AppViewModel() }
Greeting(viewModel = myViewModel,"Android")
}
}