Skip to content

Commit 6ba5719

Browse files
Fix for Fragment having constructor with parameters. (#986)
1 parent 08cef24 commit 6ba5719

6 files changed

Lines changed: 196 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## Unreleased
6+
- Fixed IterableEmbeddedView not having an empty constructor and causing crashes
7+
58
## [3.6.4]
69
### Fixed
710
- Updated `customPayload` of In-App Messages to be `@Nullable`

integration-tests/src/androidTest/java/com/iterable/integration/tests/EmbeddedMessageIntegrationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
214214
.firstOrNull() as? EmbeddedMessageTestActivity
215215

216216
if (activity != null) {
217-
val fragment = IterableEmbeddedView(IterableEmbeddedViewType.BANNER, message, null)
217+
val fragment =
218+
IterableEmbeddedView.newInstance(IterableEmbeddedViewType.BANNER, message, null)
218219
activity.supportFragmentManager.beginTransaction()
219220
.replace(R.id.embedded_message_container, fragment)
220221
.commitNow()

integration-tests/src/main/java/com/iterable/integration/tests/activities/EmbeddedMessageTestActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class EmbeddedMessageTestActivity : AppCompatActivity() {
118118

119119
if (messages.isNotEmpty()) {
120120
val firstMessage = messages.first()
121-
val fragment = IterableEmbeddedView(IterableEmbeddedViewType.BANNER, firstMessage, null)
121+
val fragment = IterableEmbeddedView.newInstance(IterableEmbeddedViewType.BANNER, firstMessage, null)
122122
supportFragmentManager.beginTransaction()
123123
.replace(R.id.embedded_message_container, fragment)
124124
.commitNowAllowingStateLoss()

iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,49 @@ import android.view.View
77
import android.view.ViewGroup
88
import android.widget.Button
99
import android.widget.ImageView
10-
import android.widget.LinearLayout
1110
import android.widget.TextView
1211
import androidx.core.content.ContextCompat
1312
import androidx.fragment.app.Fragment
1413
import com.bumptech.glide.Glide
15-
import com.google.android.flexbox.FlexboxLayout
1614
import com.iterable.iterableapi.EmbeddedMessageElementsButton
1715
import com.iterable.iterableapi.IterableApi
1816
import com.iterable.iterableapi.IterableEmbeddedMessage
1917
import com.iterable.iterableapi.ui.R
2018

21-
class IterableEmbeddedView(
22-
private var viewType: IterableEmbeddedViewType,
23-
private var message: IterableEmbeddedMessage,
24-
private var config: IterableEmbeddedViewConfig?
25-
): Fragment() {
19+
class IterableEmbeddedView() : Fragment() {
20+
21+
private lateinit var viewType: IterableEmbeddedViewType
22+
private lateinit var message: IterableEmbeddedMessage
23+
private var config: IterableEmbeddedViewConfig? = null
24+
25+
/**
26+
* @deprecated This constructor violates Android Fragment best practices and will cause crashes
27+
* when the Fragment is recreated by the system (e.g., after configuration changes or process death).
28+
* Use [newInstance] factory method instead.
29+
*
30+
* Migration example:
31+
* ```
32+
* // Old (unstable / not-recommended):
33+
* val fragment = IterableEmbeddedView(viewType, message, config)
34+
*
35+
* // New (more stable / recommended):
36+
* val fragment = IterableEmbeddedView.newInstance(viewType, message, config)
37+
* ```
38+
*
39+
* This constructor will be removed in a future version.
40+
*/
41+
@Deprecated(
42+
message = "Use newInstance() factory method instead. This constructor causes crashes when Fragment is recreated by the system.",
43+
replaceWith = ReplaceWith("IterableEmbeddedView.newInstance(viewType, message, config)"),
44+
level = DeprecationLevel.WARNING
45+
)
46+
constructor(
47+
viewType: IterableEmbeddedViewType,
48+
message: IterableEmbeddedMessage,
49+
config: IterableEmbeddedViewConfig?
50+
) : this() {
51+
arguments = IterableEmbeddedViewArguments.toBundle(viewType, message, config)
52+
}
2653

2754
private val defaultBackgroundColor : Int by lazy { getDefaultColor(viewType, R.color.notification_background_color, R.color.banner_background_color, R.color.banner_background_color) }
2855
private val defaultBorderColor : Int by lazy { getDefaultColor(viewType, R.color.notification_border_color, R.color.banner_border_color, R.color.banner_border_color) }
@@ -35,6 +62,39 @@ class IterableEmbeddedView(
3562
private val defaultBorderWidth = 1
3663
private val defaultBorderCornerRadius = 8f
3764

65+
companion object {
66+
/**
67+
* Factory method to create a new instance of IterableEmbeddedView with the required parameters.
68+
*
69+
* @param viewType The type of embedded view to display
70+
* @param message The embedded message to display
71+
* @param config Optional configuration for customizing the view appearance
72+
* @return A new instance of IterableEmbeddedView
73+
*/
74+
@JvmStatic
75+
fun newInstance(
76+
viewType: IterableEmbeddedViewType,
77+
message: IterableEmbeddedMessage,
78+
config: IterableEmbeddedViewConfig? = null
79+
): IterableEmbeddedView {
80+
return IterableEmbeddedView().apply {
81+
arguments = IterableEmbeddedViewArguments.toBundle(viewType, message, config)
82+
}
83+
}
84+
}
85+
86+
override fun onCreate(savedInstanceState: Bundle?) {
87+
super.onCreate(savedInstanceState)
88+
89+
arguments?.let { args ->
90+
viewType = IterableEmbeddedViewArguments.getViewType(args)
91+
message = IterableEmbeddedViewArguments.getMessage(args)
92+
config = IterableEmbeddedViewArguments.getConfig(args)
93+
} ?: throw IllegalStateException(
94+
"IterableEmbeddedView requires arguments. Use newInstance() factory method to create this fragment."
95+
)
96+
}
97+
3898
override fun onCreateView(
3999
inflater: LayoutInflater,
40100
container: ViewGroup?,
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.iterable.iterableapi.ui.embedded
2+
3+
import android.os.Bundle
4+
import com.iterable.iterableapi.IterableEmbeddedMessage
5+
import com.iterable.iterableapi.IterableLogger
6+
import org.json.JSONException
7+
import org.json.JSONObject
8+
9+
internal object IterableEmbeddedViewArguments {
10+
11+
private const val TAG = "IterableEmbeddedViewArgs"
12+
13+
// Argument keys
14+
private const val KEY_VIEW_TYPE = "view_type"
15+
private const val KEY_MESSAGE_JSON = "message_json"
16+
private const val KEY_BG_COLOR = "bg_color"
17+
private const val KEY_BORDER_COLOR = "border_color"
18+
private const val KEY_BORDER_WIDTH = "border_width"
19+
private const val KEY_BORDER_RADIUS = "border_radius"
20+
private const val KEY_PRIMARY_BTN_BG = "primary_btn_bg"
21+
private const val KEY_PRIMARY_BTN_TEXT = "primary_btn_text"
22+
private const val KEY_SECONDARY_BTN_BG = "secondary_btn_bg"
23+
private const val KEY_SECONDARY_BTN_TEXT = "secondary_btn_text"
24+
private const val KEY_TITLE_COLOR = "title_color"
25+
private const val KEY_BODY_COLOR = "body_color"
26+
27+
fun toBundle(
28+
viewType: IterableEmbeddedViewType,
29+
message: IterableEmbeddedMessage,
30+
config: IterableEmbeddedViewConfig?
31+
): Bundle {
32+
return Bundle().apply {
33+
putString(KEY_VIEW_TYPE, viewType.name)
34+
putString(KEY_MESSAGE_JSON, IterableEmbeddedMessage.toJSONObject(message).toString())
35+
putConfig(config)
36+
}
37+
}
38+
39+
fun getViewType(arguments: Bundle): IterableEmbeddedViewType {
40+
val viewTypeName = arguments.getString(KEY_VIEW_TYPE)
41+
return viewTypeName?.let {
42+
try {
43+
IterableEmbeddedViewType.valueOf(it)
44+
} catch (e: IllegalArgumentException) {
45+
IterableLogger.e(TAG, "Invalid view type: $it, defaulting to BANNER")
46+
IterableEmbeddedViewType.BANNER
47+
}
48+
} ?: IterableEmbeddedViewType.BANNER
49+
}
50+
51+
fun getMessage(arguments: Bundle): IterableEmbeddedMessage {
52+
val messageJsonString = arguments.getString(KEY_MESSAGE_JSON)
53+
return if (messageJsonString != null) {
54+
try {
55+
val messageJson = JSONObject(messageJsonString)
56+
IterableEmbeddedMessage.fromJSONObject(messageJson)
57+
} catch (e: JSONException) {
58+
IterableLogger.e(TAG, "Failed to parse message JSON", e)
59+
throw IllegalStateException(
60+
"IterableEmbeddedView failed to restore message from saved state. Use newInstance() factory method to create this fragment."
61+
)
62+
}
63+
} else {
64+
throw IllegalStateException(
65+
"IterableEmbeddedView requires a message argument. Use newInstance() factory method to create this fragment."
66+
)
67+
}
68+
}
69+
70+
fun getConfig(arguments: Bundle): IterableEmbeddedViewConfig? {
71+
// Check if any config properties exist
72+
val hasConfig = arguments.containsKey(KEY_BG_COLOR) ||
73+
arguments.containsKey(KEY_BORDER_COLOR) ||
74+
arguments.containsKey(KEY_BORDER_WIDTH) ||
75+
arguments.containsKey(KEY_BORDER_RADIUS) ||
76+
arguments.containsKey(KEY_PRIMARY_BTN_BG) ||
77+
arguments.containsKey(KEY_PRIMARY_BTN_TEXT) ||
78+
arguments.containsKey(KEY_SECONDARY_BTN_BG) ||
79+
arguments.containsKey(KEY_SECONDARY_BTN_TEXT) ||
80+
arguments.containsKey(KEY_TITLE_COLOR) ||
81+
arguments.containsKey(KEY_BODY_COLOR)
82+
83+
return if (hasConfig) {
84+
IterableEmbeddedViewConfig(
85+
backgroundColor = arguments.getIntOrNull(KEY_BG_COLOR),
86+
borderColor = arguments.getIntOrNull(KEY_BORDER_COLOR),
87+
borderWidth = arguments.getIntOrNull(KEY_BORDER_WIDTH),
88+
borderCornerRadius = arguments.getFloatOrNull(KEY_BORDER_RADIUS),
89+
primaryBtnBackgroundColor = arguments.getIntOrNull(KEY_PRIMARY_BTN_BG),
90+
primaryBtnTextColor = arguments.getIntOrNull(KEY_PRIMARY_BTN_TEXT),
91+
secondaryBtnBackgroundColor = arguments.getIntOrNull(KEY_SECONDARY_BTN_BG),
92+
secondaryBtnTextColor = arguments.getIntOrNull(KEY_SECONDARY_BTN_TEXT),
93+
titleTextColor = arguments.getIntOrNull(KEY_TITLE_COLOR),
94+
bodyTextColor = arguments.getIntOrNull(KEY_BODY_COLOR)
95+
)
96+
} else {
97+
null
98+
}
99+
}
100+
101+
private fun Bundle.putConfig(config: IterableEmbeddedViewConfig?) {
102+
config?.let { cfg ->
103+
cfg.backgroundColor?.let { putInt(KEY_BG_COLOR, it) }
104+
cfg.borderColor?.let { putInt(KEY_BORDER_COLOR, it) }
105+
cfg.borderWidth?.let { putInt(KEY_BORDER_WIDTH, it) }
106+
cfg.borderCornerRadius?.let { putFloat(KEY_BORDER_RADIUS, it) }
107+
cfg.primaryBtnBackgroundColor?.let { putInt(KEY_PRIMARY_BTN_BG, it) }
108+
cfg.primaryBtnTextColor?.let { putInt(KEY_PRIMARY_BTN_TEXT, it) }
109+
cfg.secondaryBtnBackgroundColor?.let { putInt(KEY_SECONDARY_BTN_BG, it) }
110+
cfg.secondaryBtnTextColor?.let { putInt(KEY_SECONDARY_BTN_TEXT, it) }
111+
cfg.titleTextColor?.let { putInt(KEY_TITLE_COLOR, it) }
112+
cfg.bodyTextColor?.let { putInt(KEY_BODY_COLOR, it) }
113+
}
114+
}
115+
116+
private fun Bundle.getIntOrNull(key: String): Int? {
117+
return if (containsKey(key)) getInt(key) else null
118+
}
119+
120+
private fun Bundle.getFloatOrNull(key: String): Float? {
121+
return if (containsKey(key)) getFloat(key) else null
122+
}
123+
}

iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.iterable.iterableapi.ui.embedded
22

3-
import android.graphics.Color
4-
53
data class IterableEmbeddedViewConfig(
64
val backgroundColor: Int?,
75
val borderColor: Int?,

0 commit comments

Comments
 (0)