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
}
}
}
}
Navigation Integration
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)
)
}
}
}
}
Deep Link Integration with Navigation
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.