Skip to main content

Advanced Integration Patterns

This guide covers advanced integration patterns and architectural approaches for implementing StorifyMe in complex Android applications.

Multi-Widget Management

Managing Multiple Widgets in Single Activity

class MultiWidgetActivity : AppCompatActivity() {
private val widgets = mutableMapOf<Long, StoriesView>()
private val widgetIds = listOf(123L, 456L, 789L)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_widget)

setupMultipleWidgets()
}

private fun setupMultipleWidgets() {
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
Log.d("MultiWidget", "Widget $widgetId loaded with ${stories.size} stories")
updateWidgetState(widgetId, WidgetState.LOADED)
}

override fun onFail(widgetId: Long, error: String) {
Log.e("MultiWidget", "Widget $widgetId failed: $error")
updateWidgetState(widgetId, WidgetState.ERROR)
}
}

widgetIds.forEach { widgetId ->
val storiesView = findViewById<StoriesView>(getViewIdForWidget(widgetId))
storiesView.widgetId = widgetId
storiesView.load()
widgets[widgetId] = storiesView
}
}

private fun updateWidgetState(widgetId: Long, state: WidgetState) {
// Update UI based on widget state
val progressBar = findViewById<ProgressBar>(getProgressBarIdForWidget(widgetId))
val errorView = findViewById<TextView>(getErrorViewIdForWidget(widgetId))

when (state) {
WidgetState.LOADING -> {
progressBar.visibility = View.VISIBLE
errorView.visibility = View.GONE
}
WidgetState.LOADED -> {
progressBar.visibility = View.GONE
errorView.visibility = View.GONE
}
WidgetState.ERROR -> {
progressBar.visibility = View.GONE
errorView.visibility = View.VISIBLE
}
}
}
}

enum class WidgetState { LOADING, LOADED, ERROR }

Widget Factory Pattern

class StorifyMeWidgetFactory {
companion object {
fun createWidget(
context: Context,
widgetId: Long,
configuration: WidgetConfiguration
): StoriesView {
val storiesView = StoriesView(context)

// Apply configuration
storiesView.widgetId = widgetId

configuration.audioOptions?.let { audioOptions ->
storiesView.setAudioOptions(audioOptions.behaviour, audioOptions.defaultState)
}

configuration.playbackOptions?.let { playbackOptions ->
storiesView.setPlaybackOptions(playbackOptions)
}

configuration.posterSettings?.let { posterSettings ->
storiesView.setGifPosterEnabled(posterSettings.gifEnabled)
storiesView.setVideoPosterEnabled(posterSettings.videoEnabled)
}

return storiesView
}
}
}

data class WidgetConfiguration(
val audioOptions: AudioConfiguration? = null,
val playbackOptions: StoryPlaybackBehaviour? = null,
val posterSettings: PosterConfiguration? = null
)

data class AudioConfiguration(
val behaviour: StoryAudioBehaviour,
val defaultState: StoryAudioState
)

data class PosterConfiguration(
val gifEnabled: Boolean = true,
val videoEnabled: Boolean = false
)

State Management Integration

With ViewModel (MVVM Pattern)

class StoriesViewModel : ViewModel() {
private val _widgetState = MutableLiveData<WidgetState>()
val widgetState: LiveData<WidgetState> = _widgetState

private val _stories = MutableLiveData<List<StoryWithSeen>>()
val stories: LiveData<List<StoryWithSeen>> = _stories

private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error

private val eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
_widgetState.postValue(WidgetState.LOADED)
_stories.postValue(stories)
}

override fun onFail(widgetId: Long, error: String) {
_widgetState.postValue(WidgetState.ERROR)
_error.postValue(error)
}

override fun onStoryOpen(widgetId: Long, storyId: Long, storyHandle: String) {
// Update analytics or state
trackStoryOpen(storyHandle)
}
}

fun initializeWidget(widgetId: Long) {
_widgetState.value = WidgetState.LOADING
StorifyMe.instance.eventListener = eventListener
}

private fun trackStoryOpen(storyHandle: String) {
// Analytics tracking
// Firebase.analytics.logEvent("story_opened", bundleOf("handle" to storyHandle))
}
}

class StoriesActivity : AppCompatActivity() {
private lateinit var viewModel: StoriesViewModel
private lateinit var storiesView: StoriesView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_stories)

viewModel = ViewModelProvider(this)[StoriesViewModel::class.java]
storiesView = findViewById(R.id.storiesView)

setupObservers()
viewModel.initializeWidget(WIDGET_ID)
}

private fun setupObservers() {
viewModel.widgetState.observe(this) { state ->
updateUI(state)
}

viewModel.stories.observe(this) { stories ->
// Handle stories loaded
storiesView.widgetId = WIDGET_ID
storiesView.load()
}

viewModel.error.observe(this) { error ->
showError(error)
}
}
}

With Repository Pattern

interface StorifyMeRepository {
suspend fun loadWidget(widgetId: Long): Result<List<StoryWithSeen>>
fun observeWidgetState(widgetId: Long): Flow<WidgetState>
}

class StorifyMeRepositoryImpl : StorifyMeRepository {
private val _widgetStates = MutableStateFlow<Map<Long, WidgetState>>(emptyMap())

override suspend fun loadWidget(widgetId: Long): Result<List<StoryWithSeen>> {
return withContext(Dispatchers.IO) {
try {
updateWidgetState(widgetId, WidgetState.LOADING)

// StorifyMe loading logic
val stories = suspendCoroutine<List<StoryWithSeen>> { continuation ->
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
updateWidgetState(widgetId, WidgetState.LOADED)
continuation.resume(stories)
}

override fun onFail(widgetId: Long, error: String) {
updateWidgetState(widgetId, WidgetState.ERROR)
continuation.resumeWithException(Exception(error))
}
}
}

Result.success(stories)
} catch (e: Exception) {
Result.failure(e)
}
}
}

override fun observeWidgetState(widgetId: Long): Flow<WidgetState> {
return _widgetStates
.map { it[widgetId] ?: WidgetState.IDLE }
.distinctUntilChanged()
}

private fun updateWidgetState(widgetId: Long, state: WidgetState) {
val currentStates = _widgetStates.value.toMutableMap()
currentStates[widgetId] = state
_widgetStates.value = currentStates
}
}

Custom Analytics Integration

Analytics Wrapper

interface AnalyticsTracker {
fun trackStoryOpen(storyHandle: String, widgetId: Long)
fun trackStoryClose(storyHandle: String, duration: Long)
fun trackStoryAction(actionType: String, actionUrl: String)
fun trackWidgetLoad(widgetId: Long, storyCount: Int, loadTime: Long)
}

class CompositeAnalyticsTracker : AnalyticsTracker {
private val trackers = listOf(
FirebaseAnalyticsTracker(),
CustomAnalyticsTracker(),
// Add more analytics providers
)

override fun trackStoryOpen(storyHandle: String, widgetId: Long) {
trackers.forEach { it.trackStoryOpen(storyHandle, widgetId) }
}

override fun trackStoryClose(storyHandle: String, duration: Long) {
trackers.forEach { it.trackStoryClose(storyHandle, duration) }
}

// ... other methods
}

class StorifyMeAnalyticsListener(
private val analyticsTracker: AnalyticsTracker
) : StorifyMeEventListener() {
private val storyOpenTimes = mutableMapOf<String, Long>()
private var widgetLoadStartTime: Long = 0

override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
val loadTime = System.currentTimeMillis() - widgetLoadStartTime
analyticsTracker.trackWidgetLoad(widgetId, stories.size, loadTime)
}

override fun onStoryOpen(widgetId: Long, storyId: Long, storyHandle: String) {
storyOpenTimes[storyHandle] = System.currentTimeMillis()
analyticsTracker.trackStoryOpen(storyHandle, widgetId)
}

override fun onStoryClose(widgetId: Long, storyId: Long, storyHandle: String) {
val openTime = storyOpenTimes[storyHandle]
if (openTime != null) {
val duration = System.currentTimeMillis() - openTime
analyticsTracker.trackStoryClose(storyHandle, duration)
storyOpenTimes.remove(storyHandle)
}
}

override fun onAction(widgetId: Long, storyId: Long, actionType: String, actionUrl: String) {
analyticsTracker.trackStoryAction(actionType, actionUrl)
}

fun startLoadTimer() {
widgetLoadStartTime = System.currentTimeMillis()
}
}

Dynamic Configuration

Configuration from Remote Source

data class RemoteWidgetConfig(
val widgetId: Long,
val enabled: Boolean,
val audioEnabled: Boolean,
val gifPostersEnabled: Boolean,
val videoPostersEnabled: Boolean,
val customTheme: String?
)

class DynamicConfigurationManager {
private val configCache = mutableMapOf<Long, RemoteWidgetConfig>()

suspend fun getWidgetConfig(widgetId: Long): RemoteWidgetConfig {
return configCache[widgetId] ?: fetchConfigFromServer(widgetId)
}

private suspend fun fetchConfigFromServer(widgetId: Long): RemoteWidgetConfig {
// Fetch from your backend/Firebase Remote Config
return withContext(Dispatchers.IO) {
// Your API call here
val config = apiService.getWidgetConfig(widgetId)
configCache[widgetId] = config
config
}
}

fun applyConfiguration(storiesView: StoriesView, config: RemoteWidgetConfig) {
if (!config.enabled) {
storiesView.visibility = View.GONE
return
}

storiesView.setAudioOptions(
if (config.audioEnabled) StoryAudioBehaviour.APPLY_LAST_USER_CHANGE_FOR_ALL_FUTURE_STORIES
else StoryAudioBehaviour.APPLY_CHANGE_FOR_SINGLE_STORY,
if (config.audioEnabled) StoryAudioState.UNMUTED else StoryAudioState.MUTED
)

storiesView.setGifPosterEnabled(config.gifPostersEnabled)
storiesView.setVideoPosterEnabled(config.videoPostersEnabled)

// Apply custom theme if available
config.customTheme?.let { theme ->
applyCustomTheme(storiesView, theme)
}
}

private fun applyCustomTheme(storiesView: StoriesView, theme: String) {
// Apply custom styling based on theme
when (theme) {
"dark" -> {
// Apply dark theme customizations
}
"brand" -> {
// Apply brand-specific customizations
}
}
}
}

With Navigation Component

class StoriesFragment : Fragment() {
private lateinit var storiesView: StoriesView
private val args: StoriesFragmentArgs by navArgs()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_stories, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

storiesView = view.findViewById(R.id.storiesView)
setupStoriesView()
}

private fun setupStoriesView() {
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onAction(widgetId: Long, storyId: Long, actionType: String, actionUrl: String) {
handleStoryAction(actionType, actionUrl)
}
}

storiesView.widgetId = args.widgetId
storiesView.load()
}

private fun handleStoryAction(actionType: String, actionUrl: String) {
when (actionType) {
"navigate" -> {
// Navigate using Navigation Component
findNavController().navigate(
StoriesFragmentDirections.actionStoriesFragmentToDetailFragment(actionUrl)
)
}
"open_product" -> {
// Navigate to product detail
val productId = extractProductId(actionUrl)
findNavController().navigate(
StoriesFragmentDirections.actionStoriesFragmentToProductFragment(productId)
)
}
}
}
}
class DeepLinkHandler {
fun handleStorifyMeDeepLink(context: Context, uri: Uri): Boolean {
val handle = extractStoryHandle(uri)
if (handle != null) {
// Try widget-based opening first
val currentFragment = getCurrentFragment(context)
if (currentFragment is StoriesFragment) {
currentFragment.openStoryByHandle(handle)
return true
}

// Fall back to direct opening
PreviewStoryByHandleLauncher.INSTANCE.launch(context, handle)
return true
}
return false
}

private fun getCurrentFragment(context: Context): Fragment? {
if (context is AppCompatActivity) {
return context.supportFragmentManager.primaryNavigationFragment
}
return null
}
}

Testing Patterns

Unit Testing with Mocks

class StorifyMeTestHelper {
companion object {
fun createMockEventListener(): StorifyMeEventListener {
return object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
// Mock implementation
}

override fun onFail(widgetId: Long, error: String) {
// Mock implementation
}
}
}

fun createMockStories(): List<StoryWithSeen> {
return listOf(
// Create mock story objects for testing
)
}
}
}

@RunWith(MockitoJUnitRunner::class)
class StoriesViewModelTest {
@Mock
private lateinit var analyticsTracker: AnalyticsTracker

private lateinit var viewModel: StoriesViewModel

@Before
fun setup() {
viewModel = StoriesViewModel(analyticsTracker)
}

@Test
fun `should update state when widget loads successfully`() {
// Given
val testObserver = viewModel.widgetState.test()
val mockStories = StorifyMeTestHelper.createMockStories()

// When
viewModel.initializeWidget(123L)
// Simulate onLoad callback
viewModel.handleWidgetLoad(123L, mockStories)

// Then
testObserver.assertValues(WidgetState.LOADING, WidgetState.LOADED)
}
}

Integration Testing

@RunWith(AndroidJUnit4::class)
class StoriesIntegrationTest {
@get:Rule
val activityRule = ActivityTestRule(StoriesActivity::class.java, false, false)

@Test
fun testWidgetLoadsAndDisplaysStories() {
// Launch activity
val activity = activityRule.launchActivity(null)

// Wait for widget to load
Espresso.onView(ViewMatchers.withId(R.id.storiesView))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))

// Verify stories are displayed
Espresso.onView(ViewMatchers.withId(R.id.storiesView))
.check { view, noViewFoundException ->
if (noViewFoundException != null) {
throw noViewFoundException
}

val storiesView = view as StoriesView
// Add assertions for loaded state
}
}
}

Memory Management Patterns

Widget Lifecycle Management

class StoriesLifecycleManager : DefaultLifecycleObserver {
private val activeWidgets = mutableSetOf<StoriesView>()

override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
// Resume any paused widgets
activeWidgets.forEach { widget ->
// Resume if SDK provides method
}
}

override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
// Pause widgets to save resources
activeWidgets.forEach { widget ->
// Pause if SDK provides method
}
}

override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
// Clean up resources
activeWidgets.forEach { widget ->
// Clean up if SDK provides method
}
activeWidgets.clear()
}

fun registerWidget(widget: StoriesView) {
activeWidgets.add(widget)
}

fun unregisterWidget(widget: StoriesView) {
activeWidgets.remove(widget)
}
}

These patterns provide a solid foundation for integrating StorifyMe into complex Android applications while maintaining clean architecture, testability, and performance.