-
Couldn't load subscription status.
- Fork 10
Initial library setup #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/content-card
Are you sure you want to change the base?
Changes from all commits
3b0656a
e5b41f9
c5370ea
6282a8e
027e071
32feb97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /build |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| plugins { | ||
| id("aep-library") | ||
| } | ||
|
|
||
| val mavenCoreVersion: String by project | ||
| val aepComposeUiModuleName: String by project | ||
| val aepComposeUiVersion: String by project | ||
| val aepComposeUiMavenRepoName: String by project | ||
| val aepComposeUiMavenRepoDescription: String by project | ||
|
|
||
| aepLibrary { | ||
| namespace = "com.adobe.marketing.mobile.aepcomposeui" | ||
|
|
||
| moduleName = aepComposeUiModuleName | ||
| moduleVersion = aepComposeUiVersion | ||
| enableSpotless = true | ||
| enableCheckStyle = true | ||
| enableDokkaDoc = true | ||
|
|
||
| publishing { | ||
| mavenRepoName = aepComposeUiMavenRepoName | ||
| mavenRepoDescription = aepComposeUiMavenRepoDescription | ||
| gitRepoName = "aepsdk-ui-android" | ||
| addCoreDependency(mavenCoreVersion) | ||
| } | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation("com.adobe.marketing.mobile:core:$mavenCoreVersion") | ||
| testImplementation("org.robolectric:robolectric:4.7") | ||
| testImplementation("io.mockk:mockk:1.13.11") | ||
| api(project(":aepuitemplates")) | ||
| implementation(platform(libs.androidx.compose.bom)) | ||
| implementation(libs.androidx.ui) | ||
| implementation(libs.androidx.ui.graphics) | ||
| implementation(libs.androidx.ui.tooling.preview) | ||
| implementation(libs.androidx.material3) | ||
| implementation(libs.androidx.lifecycle.viewmodel.compose) | ||
| implementation(libs.androidx.lifecycle.runtime.compose) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.adobe.marketing.mobile.aepcomposeui | ||
|
|
||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
|
|
||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
|
|
||
| import org.junit.Assert.* | ||
|
|
||
| /** | ||
| * Instrumented test, which will execute on an Android device. | ||
| * | ||
| * See [testing documentation](http://d.android.com/tools/testing). | ||
| */ | ||
| @RunWith(AndroidJUnit4::class) | ||
| class ExampleInstrumentedTest { | ||
| @Test | ||
| fun useAppContext() { | ||
| // Context of the app under test. | ||
| val appContext = InstrumentationRegistry.getInstrumentation().targetContext | ||
| assertEquals("com.adobe.marketing.mobile.aepcomposeui.test", appContext.packageName) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| </manifest> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.adobe.marketing.mobile.aepcomposeui.aepui | ||
|
|
||
| import androidx.compose.runtime.State | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import com.adobe.marketing.mobile.aepuitemplates.AepUiTemplate | ||
| import com.adobe.marketing.mobile.aepuitemplates.SmallImageTemplate | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.state.AepUiState | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.state.SmallImageUIState | ||
|
|
||
|
|
||
| /** | ||
| * Represents a UI component that can be rendered in the AepUI Engine. Binds a template data with a tracking observer. | ||
| * This is a sealed interface that can be implemented by different UI components like [SmallImageAepUi], [LargeImageAepUi], etc. | ||
| * This allows restricting the type of template and observer that can be associated with a UI component, which can be later | ||
| * used in the app to render the UI component via UIComposeExtensions. | ||
| * @param T The type of the template associated with the UI component. | ||
| * @param S The type of the state associated with the UI component. | ||
| */ | ||
| sealed interface AepUI<T : AepUiTemplate, S : AepUiState> { | ||
| fun getTemplate(): T | ||
| fun getState(): S | ||
| fun updateState(newState: S) | ||
| } | ||
|
|
||
|
|
||
| class SmallImageAepUi( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move this to its own file? Can we decide if it needs to be Can we also change |
||
| private val template: SmallImageTemplate, | ||
| state: SmallImageUIState | ||
| ) : AepUI<SmallImageTemplate, SmallImageUIState> { | ||
| private val _state = mutableStateOf(state) | ||
| override fun updateState(newState: SmallImageUIState) { | ||
| _state.value = newState | ||
| } | ||
|
|
||
| override fun getTemplate(): SmallImageTemplate { | ||
| return template | ||
| } | ||
|
|
||
| override fun getState(): SmallImageUIState { | ||
| return _state.value | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package com.adobe.marketing.mobile.aepcomposeui.aepui.components | ||
|
|
||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.lazy.LazyColumn | ||
| import androidx.compose.material3.Button | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.unit.dp | ||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.ViewModelProvider | ||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import androidx.lifecycle.viewModelScope | ||
| import androidx.lifecycle.viewmodel.compose.viewModel | ||
| import com.adobe.marketing.mobile.aepuitemplates.SmallImageTemplate | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.AepUI | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.SmallImageAepUi | ||
| import com.adobe.marketing.mobile.aepcomposeui.contentprovider.AepUiContentProvider | ||
| import com.adobe.marketing.mobile.aepcomposeui.observers.AepUiEventObserver | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.state.SmallImageUIState | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.style.AepUiStyle | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| /** | ||
| * Composable for rendering a list of AEP UI components. | ||
| * Maintains a list of AEP UI components and renders them using the provided container. | ||
| */ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add descriptions of what the |
||
| @Composable | ||
| fun AepList( | ||
| contentProvider: AepUiContentProvider, | ||
| aepUiEventObserver: AepUiEventObserver, | ||
| aepUiStyle: AepUiStyle, | ||
| viewModelKey: String, | ||
| container: @Composable (@Composable () -> Unit) -> Unit = { content -> | ||
| // Default to a Column if no container is provided | ||
| LazyColumn( | ||
| verticalArrangement = Arrangement.spacedBy(8.dp), | ||
| horizontalAlignment = Alignment.CenterHorizontally | ||
| ) { | ||
| item { | ||
| content() | ||
| } | ||
| } | ||
| } | ||
| ) { | ||
| val viewModel: AepListViewModel = viewModel( | ||
| factory = AepComposableViewModelFactory( | ||
| contentProvider | ||
| ), | ||
| key = viewModelKey | ||
| ) | ||
| val uiList = viewModel.uiList.collectAsStateWithLifecycle() | ||
|
|
||
| container { | ||
| uiList.value.forEach { ui -> | ||
| val uiAsComposable = asComposable(ui, aepUiEventObserver, aepUiStyle) | ||
| uiAsComposable.invoke() | ||
| } | ||
|
|
||
| Button(onClick = { viewModel.loadMore() }, Modifier.fillMaxWidth()) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This button was added for testing. We don't need it in the actual implementation |
||
| Text(text = "Load More") | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private fun asComposable( | ||
| aepUI: AepUI<*, *>, | ||
| observer: AepUiEventObserver, | ||
| aepUiStyle: AepUiStyle | ||
| ): @Composable () -> Unit { | ||
| return when (aepUI) { | ||
| is SmallImageAepUi -> { | ||
| { | ||
| val state = aepUI.getState() | ||
| if (!state.dismissed) { | ||
| SmallImageCard( | ||
| ui = aepUI, | ||
| style = aepUiStyle.smallImageAepUiStyle, | ||
| observer = observer | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| else -> throw IllegalArgumentException("Unknown template type") | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private class AepListViewModel( | ||
| private val contentProvider: AepUiContentProvider, | ||
| ) : ViewModel() { | ||
|
|
||
| private val _uiList = MutableStateFlow(listOf<AepUI<*, *>>()) | ||
| val uiList: StateFlow<List<AepUI<*, *>>> = _uiList | ||
|
|
||
| init { | ||
| viewModelScope.launch { | ||
| contentProvider.getContent().collect { templates -> | ||
| val uiList = templates.map { template -> | ||
|
|
||
| val aepUiState: AepUI<*, *> = when (template) { | ||
| is SmallImageTemplate -> SmallImageAepUi( | ||
| template, | ||
| SmallImageUIState(title = template.title) | ||
| ) | ||
| else -> throw IllegalArgumentException("Unknown template type") | ||
| } | ||
|
|
||
| aepUiState | ||
| } | ||
| _uiList.value = uiList | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fun loadMore() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we call this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also we don't need to support this right now. It was just there for POC. We can add it later if needed |
||
| viewModelScope.launch { | ||
| contentProvider.refreshContent() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private class AepComposableViewModelFactory( | ||
| private val contentProvider: AepUiContentProvider | ||
| ) : ViewModelProvider.Factory { | ||
| override fun <T : ViewModel> create(modelClass: Class<T>): T { | ||
| if (modelClass.isAssignableFrom(AepListViewModel::class.java)) { | ||
| @Suppress("UNCHECKED_CAST") | ||
| return AepListViewModel(contentProvider) as T | ||
| } | ||
| throw IllegalArgumentException("Unknown ViewModel class") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package com.adobe.marketing.mobile.aepcomposeui.aepui.components | ||
|
|
||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.width | ||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.material3.Card | ||
| import androidx.compose.material3.CardDefaults | ||
| import androidx.compose.material3.MaterialTheme | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.DisposableEffect | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.unit.dp | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.SmallImageAepUi | ||
| import com.adobe.marketing.mobile.aepcomposeui.aepui.style.SmallImageAepUiStyle | ||
| import com.adobe.marketing.mobile.aepcomposeui.interactions.UIEvent | ||
| import com.adobe.marketing.mobile.aepcomposeui.observers.AepUiEventObserver | ||
|
|
||
| /** | ||
| * Composable for rendering a Small Image Card | ||
| */ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add description for |
||
| @Composable | ||
| internal fun SmallImageCard( | ||
| ui: SmallImageAepUi, | ||
| style: SmallImageAepUiStyle, | ||
| observer: AepUiEventObserver?, | ||
| ) { | ||
|
|
||
| LaunchedEffect(key1 = Unit) { | ||
| observer?.onEvent(UIEvent.Display(ui)) | ||
| } | ||
|
|
||
| DisposableEffect(key1 = Unit) { | ||
| onDispose { | ||
| observer?.onEvent(UIEvent.Dismiss(ui)) | ||
| } | ||
| } | ||
|
|
||
| Card( | ||
| shape = RoundedCornerShape(8.dp), | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(16.dp) | ||
| .clickable { | ||
| observer?.onEvent(UIEvent.Click(ui)) | ||
| }, | ||
| elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) | ||
| ) { | ||
| Row( | ||
| modifier = Modifier.padding(16.dp), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| // TODO - Add image support | ||
| Spacer(modifier = Modifier.width(16.dp)) | ||
|
|
||
| Column( | ||
| verticalArrangement = Arrangement.Center | ||
| ) { | ||
| ui.getState().title?.let { | ||
| Text( | ||
| text = it, | ||
| style = style.getTitleTextStyle(ui.getTemplate()), | ||
| ) | ||
| } | ||
| Text( | ||
| text = ui.getTemplate().description, | ||
| style = MaterialTheme.typography.bodyMedium | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.adobe.marketing.mobile.aepcomposeui.aepui.state | ||
|
|
||
| sealed interface AepUiState | ||
|
|
||
| data class SmallImageUIState(val title: String?, val dismissed: Boolean = false) : AepUiState | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move this to its own file |
||
| data class LargeImageUIState(val dismissed: Boolean = false) : AepUiState | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.adobe.marketing.mobile.aepcomposeui.aepui.style | ||
|
|
||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.text.TextStyle | ||
| import androidx.compose.ui.text.style.TextAlign | ||
|
|
||
| // only needed when we support server side styling | ||
| fun AEPText.getComposeTextStyle(): TextStyle { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove this class? There's no support for server side styling in AJO. We can add it when we need to |
||
| var textStyle = TextStyle() | ||
| if (color != null) { | ||
| textStyle = textStyle.merge(Color(android.graphics.Color.parseColor(color))) | ||
| } | ||
| if (align != null) { | ||
| textStyle = textStyle.merge(textAlign = when (align) { | ||
| "center" -> TextAlign.Center | ||
| "left" -> TextAlign.Left | ||
| "right" -> TextAlign.Right | ||
| "start" -> TextAlign.Start | ||
| "end" -> TextAlign.End | ||
| else -> TextAlign.Unspecified | ||
| }) | ||
| } | ||
| return textStyle | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need core dependency here?