-
Hello, I'm trying to implement onboarding with few pages. Some of the pages requires location or other kind of device permission which are platform specific. I'm not sure what will be the most elegant way to leave some of the platform specific implementation. Shall I use All my pages looks very similar that's why I created common onboarding component with array of interface Onboarding {
val model: Value<Model>
fun onNextPageClicked(currentPage: StepPage, nextPage: StepPage?)
enum class Step
@Parcelize
data class StepPage(val step: Step, val title: String, val description: String, val nextButtonTitle: String) :
Parcelable
@Parcelize
data class Model(val steps: Array<StepPage>) : Parcelable
}
class OnboardingComponent(
componentContext: ComponentContext,
private val steps: Array<Onboarding.StepPage>,
private val onNext: (current: Onboarding.StepPage, next: Onboarding.StepPage?) -> Unit
) : Onboarding, ComponentContext by componentContext {
override val model: Value<Onboarding.Model>
get() = MutableValue(Onboarding.Model(steps))
override fun onNextPageClicked(currentPage: Onboarding.StepPage, nextPage: Onboarding.StepPage?) {
onNext(currentPage, nextPage)
}
} Unfortunately I should lift the callbacks and the array list to the Here how looks the Android part: val onboarding = arrayOf(Onboarding.StepPage(...), Onboarding.StepPage(...))
val root = RootComponent(componentContext = defaultComponentContext(), onboarding) { current, next ->
if (next != null) {
// no idea how to navigate here
}
// check current and if needed request permission
}
setContent {
AppTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
RootContent(root, modifier = Modifier.fillMaxSize())
}
}
} Is there any way to handle the switch between steps and the platform specific (request permissions) not on the top level but inside a Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 4 replies
-
Hello! I would recommend to define an interface in interface PermissionRequester {
fun register(onResult: (response: Map<Permission, Boolean>) -> Unit): Request
interface Request {
fun request(vararg permissions: Permission)
}
enum class Permission {
LOCATION,
EXTERNAL_STORAGE,
}
} This interface can be easily implemented on Android - see the API documentation. I didn't try it implementing on iOS, but I guess it should be also doable. Please let me know how it goes! Then your component can look like this. interface Onboarding {
val model: Value<Model>
fun onNextPageClicked()
enum class Step
data class StepPage(val step: Step, val title: String, val description: String, val nextButtonTitle: String)
data class Model(
val steps: List<StepPage>,
val stepIndex: Int,
)
}
class OnboardingComponent(
componentContext: ComponentContext,
steps: List<Onboarding.StepPage>,
permissionRequester: PermissionRequester,
private val onFinished: () -> Unit, // Called when no steps left
) : Onboarding, ComponentContext by componentContext {
override val model: Value<Onboarding.Model> = MutableValue(Onboarding.Model(steps = steps, stepIndex = 0))
private val permissionRequest =
permissionRequester.register { response ->
// Check the response
// Update the model with the next step
}
override fun onNextPageClicked() {
// Check current and next page
// Request permissions or update the model with the next step
// permissionRequest.request()
}
} Please note:
|
Beta Was this translation helpful? Give feedback.
-
Hey @arkivanov thanks for the nice proposals. I also thinking about KMM Permissions library and I found one MOKO permissions library, but it looks doesn't providing support for Jetpack Compose and for me looks hard to configure. That's why I decided to leave the permissions logic for now into the platform specific files until I understand better the Decompose and everything. How I did it: First I define pages like this: interface Page {
val model: Value<Model>
fun onNextPageClicked()
@Parcelize
data class Model(
val step: PageStep,
val title: String,
val description: String,
val buttonText: String,
val nextPageStep: PageStep? = null
) : Parcelable
}
class PageComponent(
componentContext: ComponentContext,
private val page: Page.Model,
private val onNextPage: () -> Unit
) : Page, ComponentContext by componentContext {
override val model: Value<Page.Model>
get() = MutableValue(page)
override fun onNextPageClicked() {
onNextPage()
}
}
interface Boarding {
val childStack: Value<ChildStack<*, Page>>
}
class BoardingComponent(componentContext: CustomComponentContext, private val onFinished: () -> Unit) :
Boarding,
CustomComponentContext by componentContext {
private val navigation = StackNavigation<Page.Model>()
override val childStack = customChildStack(
source = navigation,
initialConfiguration = boarding.first(),
key = "BoardingStack",
childFactory = ::createChild,
)
private fun createChild(page: Page.Model, componentContext: ComponentContext) =
PageComponent(componentContext, page) {
page.nextPageStep?.let { next ->
navigation.push(componentContext.boarding.first { it.step == next })
} ?: onFinished()
}
} The @OptIn(ExperimentalDecomposeApi::class, ExperimentalPermissionsApi::class)
@Composable
internal fun BoardingContent(component: Boarding, modifier: Modifier = Modifier) {
val childStack by component.childStack.subscribeAsState()
val activeComponent = childStack.active.instance
val model by activeComponent.model.subscribeAsState()
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(modifier = Modifier, scaffoldState = scaffoldState) { padding ->
Column(modifier = modifier.padding(padding)) {
Children(
stack = childStack,
modifier = Modifier.weight(weight = 1F),
) {
val permissions = when (model.step) {
PageStep.PROPERTY_LOCATION, PageStep.SEARCH_LOCATION -> listOf(
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION
)
PageStep.BOOKING_NOTIFICATIONS, PageStep.INVITES_NOTIFICATIONS -> if (Build.VERSION.SDK_INT >= 33) {
listOf(
android.Manifest.permission.POST_NOTIFICATIONS
)
} else {
listOf()
}
else -> listOf()
}
val permissionState = rememberMultiplePermissionsState(permissions) {
if (it.values.all { it }) {
activeComponent.onNextPageClicked()
}
}
ActionButtonScreen(text = model.buttonText, onActionClicked = {
if (permissionState.allPermissionsGranted) {
activeComponent.onNextPageClicked()
return@ActionButtonScreen
}
if (permissionState.shouldShowRationale) {
coroutineScope.launch {
when (scaffoldState.snackbarHostState.showSnackbar("Permission message", "Grant")) {
SnackbarResult.ActionPerformed -> permissionState.launchMultiplePermissionRequest()
else -> {}
}
}
} else {
permissionState.launchMultiplePermissionRequest()
}
}) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(80.dp),
painter = painterResource(id = model.step.drawable()),
contentDescription = model.description
)
Spacer(modifier = Modifier.padding(top = 24.dp))
Text(
text = model.title,
style = MaterialTheme.typography.subtitle1.plus(TextStyle(fontWeight = FontWeight.Bold)),
)
Spacer(modifier = Modifier.padding(top = 16.dp))
Text(text = model.description, style = MaterialTheme.typography.caption)
}
}
}
}
}
} I know with the declarative UIs and Decompose you have a bunch of options to implement a feature but what do you think about my approach, any suggestions, ideas to improve @arkivanov? |
Beta Was this translation helpful? Give feedback.
Hey @arkivanov thanks for the nice proposals. I also thinking about KMM Permissions library and I found one MOKO permissions library, but it looks doesn't providing support for Jetpack Compose and for me looks hard to configure. That's why I decided to leave the permissions logic for now into the platform specific files until I understand better the Decompose and everything.
How I did it:
First I define pages like this: