隔离:最终版本
This commit is contained in:
parent
6747e97cf9
commit
cb8f9dc085
@ -10,6 +10,12 @@
|
|||||||
android:maxSdkVersion="32"
|
android:maxSdkVersion="32"
|
||||||
tools:replace="android:maxSdkVersion" />
|
tools:replace="android:maxSdkVersion" />
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="fb" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
android:label="PetsHero AI"
|
android:label="PetsHero AI"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.9",
|
"version": "2.11",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
@ -499,7 +499,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156213563.png",
|
"url": "images/generated-1773156213563.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"layout": "none",
|
"layout": "none",
|
||||||
@ -667,7 +667,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156252029.png",
|
"url": "images/generated-1773156252029.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"layout": "none",
|
"layout": "none",
|
||||||
@ -825,7 +825,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156289353.png",
|
"url": "images/generated-1773156289353.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"layout": "none",
|
"layout": "none",
|
||||||
@ -993,7 +993,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156316903.png",
|
"url": "images/generated-1773156316903.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"layout": "none",
|
"layout": "none",
|
||||||
@ -1151,7 +1151,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156352910.png",
|
"url": "images/generated-1773156352910.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"layout": "none",
|
"layout": "none",
|
||||||
@ -1708,7 +1708,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156157712.png",
|
"url": "images/generated-1773156157712.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 24,
|
"cornerRadius": 24,
|
||||||
@ -1805,7 +1805,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156363524.png",
|
"url": "images/generated-1773156363524.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 24,
|
"cornerRadius": 24,
|
||||||
@ -1912,7 +1912,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156393690.png",
|
"url": "images/generated-1773156393690.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 24,
|
"cornerRadius": 24,
|
||||||
@ -2009,7 +2009,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773134913667.png",
|
"url": "images/generated-1773134913667.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 24,
|
"cornerRadius": 24,
|
||||||
@ -2948,10 +2948,37 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"id": "Lt3TV",
|
"id": "omZCh",
|
||||||
"name": "navSpacer",
|
"name": "recordEntry",
|
||||||
"width": 40,
|
"height": 40,
|
||||||
"height": 40
|
"gap": 4,
|
||||||
|
"padding": [
|
||||||
|
0,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "X2yWW",
|
||||||
|
"name": "recordEntryIcon",
|
||||||
|
"width": 16,
|
||||||
|
"height": 16,
|
||||||
|
"iconFontName": "list",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#8B5CF6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "fjE0S",
|
||||||
|
"name": "recordEntryText",
|
||||||
|
"fill": "#8B5CF6",
|
||||||
|
"content": "History",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 13,
|
||||||
|
"fontWeight": "600"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -3941,7 +3968,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/pet-hero-1.png",
|
"url": "images/pet-hero-1.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 16,
|
"cornerRadius": 16,
|
||||||
@ -4281,7 +4308,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156472555.png",
|
"url": "images/generated-1773156472555.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 16,
|
"cornerRadius": 16,
|
||||||
@ -4513,7 +4540,7 @@
|
|||||||
"fill": {
|
"fill": {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "./images/generated-1773156511265.png",
|
"url": "images/generated-1773156511265.png",
|
||||||
"mode": "fill"
|
"mode": "fill"
|
||||||
},
|
},
|
||||||
"cornerRadius": 16,
|
"cornerRadius": 16,
|
||||||
@ -5017,6 +5044,373 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "HhcCM",
|
||||||
|
"x": 3560,
|
||||||
|
"y": 0,
|
||||||
|
"name": "积分记录页面",
|
||||||
|
"clip": true,
|
||||||
|
"width": 390,
|
||||||
|
"height": 844,
|
||||||
|
"fill": "#FAFAFA",
|
||||||
|
"layout": "vertical",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "QtoLk",
|
||||||
|
"name": "statusBar",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 40,
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"padding": [
|
||||||
|
0,
|
||||||
|
12,
|
||||||
|
0,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "b5Ogx",
|
||||||
|
"name": "recordTime",
|
||||||
|
"fill": "#18181B",
|
||||||
|
"content": "9:41",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 13,
|
||||||
|
"fontWeight": "600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "Su906",
|
||||||
|
"name": "recordStatusIcons",
|
||||||
|
"gap": 6,
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "MTrqx",
|
||||||
|
"name": "recordSignal",
|
||||||
|
"width": 21,
|
||||||
|
"height": 15,
|
||||||
|
"iconFontName": "signal",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#18181B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "fnm42",
|
||||||
|
"name": "recordWifi",
|
||||||
|
"width": 21,
|
||||||
|
"height": 15,
|
||||||
|
"iconFontName": "wifi",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#18181B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "nx60Z",
|
||||||
|
"name": "recordBattery",
|
||||||
|
"width": 30,
|
||||||
|
"height": 15,
|
||||||
|
"iconFontName": "battery-medium",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#18181B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "YfTGP",
|
||||||
|
"name": "topNav",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": 56,
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"effect": {
|
||||||
|
"type": "shadow",
|
||||||
|
"shadowType": "outer",
|
||||||
|
"color": "#00000008",
|
||||||
|
"offset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 2
|
||||||
|
},
|
||||||
|
"blur": 8
|
||||||
|
},
|
||||||
|
"padding": [
|
||||||
|
0,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "Napgn",
|
||||||
|
"name": "backBtn",
|
||||||
|
"width": 40,
|
||||||
|
"height": 40,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "icon_font",
|
||||||
|
"id": "JQ8oi",
|
||||||
|
"name": "recordBackIcon",
|
||||||
|
"width": 24,
|
||||||
|
"height": 24,
|
||||||
|
"iconFontName": "arrow-left",
|
||||||
|
"iconFontFamily": "lucide",
|
||||||
|
"fill": "#18181B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "vRkK0",
|
||||||
|
"name": "recordTitle",
|
||||||
|
"fill": "#18181B",
|
||||||
|
"content": "Points History",
|
||||||
|
"fontFamily": "Plus Jakarta Sans",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "hyBiG",
|
||||||
|
"name": "recordSpacer",
|
||||||
|
"width": 40,
|
||||||
|
"height": 40
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "yc0Rz",
|
||||||
|
"name": "content",
|
||||||
|
"width": "fill_container",
|
||||||
|
"height": "fill_container",
|
||||||
|
"fill": "#FAFAFA",
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 16,
|
||||||
|
"padding": [
|
||||||
|
20,
|
||||||
|
20,
|
||||||
|
24,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "pBfPr",
|
||||||
|
"name": "summaryCard",
|
||||||
|
"width": "fill_container",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 16,
|
||||||
|
"stroke": {
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E4E4E7"
|
||||||
|
},
|
||||||
|
"effect": {
|
||||||
|
"type": "shadow",
|
||||||
|
"shadowType": "outer",
|
||||||
|
"color": "#00000008",
|
||||||
|
"offset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 2
|
||||||
|
},
|
||||||
|
"blur": 6
|
||||||
|
},
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 8,
|
||||||
|
"padding": [
|
||||||
|
16,
|
||||||
|
18
|
||||||
|
],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "aNlTU",
|
||||||
|
"name": "recordSummaryTitle",
|
||||||
|
"fill": "#71717A",
|
||||||
|
"content": "Current Points",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": "500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "Od6F1",
|
||||||
|
"name": "recordSummaryValue",
|
||||||
|
"fill": "#18181B",
|
||||||
|
"content": "1,280",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 30,
|
||||||
|
"fontWeight": "700"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "v25H1",
|
||||||
|
"name": "recordList",
|
||||||
|
"width": "fill_container",
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 8,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "BzXpQ",
|
||||||
|
"name": "recordItem1",
|
||||||
|
"width": "fill_container",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 12,
|
||||||
|
"stroke": {
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E4E4E7"
|
||||||
|
},
|
||||||
|
"padding": [
|
||||||
|
14,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "r3dpe",
|
||||||
|
"name": "recordItem1Left",
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 4,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "py0Ct",
|
||||||
|
"name": "recordItem1Time",
|
||||||
|
"fill": "#71717A",
|
||||||
|
"content": "2026-04-28 09:12",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "tWrM8",
|
||||||
|
"name": "recordItem1Change",
|
||||||
|
"fill": "#16A34A",
|
||||||
|
"content": "+20",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "700"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "Jb2ZB",
|
||||||
|
"name": "recordItem2",
|
||||||
|
"width": "fill_container",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 12,
|
||||||
|
"stroke": {
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E4E4E7"
|
||||||
|
},
|
||||||
|
"padding": [
|
||||||
|
14,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "elr68",
|
||||||
|
"name": "recordItem2Left",
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 4,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "riFsx",
|
||||||
|
"name": "recordItem2Time",
|
||||||
|
"fill": "#71717A",
|
||||||
|
"content": "2026-04-27 22:45",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "j8PFQw",
|
||||||
|
"name": "recordItem2Change",
|
||||||
|
"fill": "#DC2626",
|
||||||
|
"content": "-120",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "700"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "gIDh0",
|
||||||
|
"name": "recordItem3",
|
||||||
|
"width": "fill_container",
|
||||||
|
"fill": "#FFFFFF",
|
||||||
|
"cornerRadius": 12,
|
||||||
|
"stroke": {
|
||||||
|
"thickness": 1,
|
||||||
|
"fill": "#E4E4E7"
|
||||||
|
},
|
||||||
|
"padding": [
|
||||||
|
14,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"justifyContent": "space_between",
|
||||||
|
"alignItems": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "q8BKB8",
|
||||||
|
"name": "recordItem3Left",
|
||||||
|
"layout": "vertical",
|
||||||
|
"gap": 4,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "q7lwb",
|
||||||
|
"name": "recordItem3Time",
|
||||||
|
"fill": "#71717A",
|
||||||
|
"content": "2026-04-26 14:08",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 12
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "zskzV",
|
||||||
|
"name": "recordItem3Change",
|
||||||
|
"fill": "#16A34A",
|
||||||
|
"content": "+500",
|
||||||
|
"fontFamily": "Inter",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "700"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
679
docs/rewrite_refactor_playbook.md
Normal file
679
docs/rewrite_refactor_playbook.md
Normal file
@ -0,0 +1,679 @@
|
|||||||
|
# Flutter Project Rewrite Refactor Playbook
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document adapts the generic "rewrite by a different engineer" refactor template to the current `pets_hero_ai` Flutter app.
|
||||||
|
|
||||||
|
The goal is a structural rewrite that makes the project look and feel newly organized while preserving behavior:
|
||||||
|
|
||||||
|
- UI output stays visually equivalent.
|
||||||
|
- API requests, parameters, encryption, authentication, payment, attribution, and analytics behavior stay equivalent.
|
||||||
|
- Existing app startup, login, referrer, home loading, generation, gallery, recharge, and profile flows keep working.
|
||||||
|
- The project compiles after each module migration.
|
||||||
|
|
||||||
|
This is not a feature rewrite. It is a controlled architecture migration.
|
||||||
|
|
||||||
|
## Current Project Shape
|
||||||
|
|
||||||
|
Current code is already partially feature-first:
|
||||||
|
|
||||||
|
- `lib/core/`
|
||||||
|
- API client, proxy encryption, auth, token storage, referrer, Adjust/Facebook events, theme, logging, user state.
|
||||||
|
- `lib/features/`
|
||||||
|
- `home`, `gallery`, `generate_video`, `recharge`, `profile`.
|
||||||
|
- `lib/shared/`
|
||||||
|
- shared tab selector and common widgets.
|
||||||
|
- Entrypoints:
|
||||||
|
- `lib/main.dart`
|
||||||
|
- `lib/app.dart`
|
||||||
|
|
||||||
|
Because the app already uses a `core/features/shared` layout, a plain Feature-first migration would not be different enough. The recommended target architecture is therefore a Clean Architecture-lite layout with vertical modules.
|
||||||
|
|
||||||
|
## Recommended Architecture
|
||||||
|
|
||||||
|
Choose this as the default unless there is a strong reason not to:
|
||||||
|
|
||||||
|
**Clean Architecture-lite + Vertical Modules**
|
||||||
|
|
||||||
|
Target layout:
|
||||||
|
|
||||||
|
```text
|
||||||
|
lib/
|
||||||
|
bootstrap/
|
||||||
|
main_bootstrap.dart
|
||||||
|
platform_startup.dart
|
||||||
|
|
||||||
|
app_shell/
|
||||||
|
pets_hero_app.dart
|
||||||
|
routes/
|
||||||
|
route_table.dart
|
||||||
|
route_observer.dart
|
||||||
|
lifecycle/
|
||||||
|
facebook_lifecycle_reporter.dart
|
||||||
|
|
||||||
|
foundation/
|
||||||
|
analytics/
|
||||||
|
adjust_event_gateway.dart
|
||||||
|
facebook_event_gateway.dart
|
||||||
|
auth/
|
||||||
|
device_identity_provider.dart
|
||||||
|
session_bootstrapper.dart
|
||||||
|
token_vault.dart
|
||||||
|
config/
|
||||||
|
api_environment.dart
|
||||||
|
facebook_environment.dart
|
||||||
|
logging/
|
||||||
|
tagged_log.dart
|
||||||
|
network/
|
||||||
|
api_response.dart
|
||||||
|
encrypted_proxy_client.dart
|
||||||
|
proxy_crypto.dart
|
||||||
|
service_locator.dart
|
||||||
|
referrer/
|
||||||
|
install_referrer_probe.dart
|
||||||
|
attribution_digest_builder.dart
|
||||||
|
attribution_polling_coordinator.dart
|
||||||
|
theme/
|
||||||
|
colors.dart
|
||||||
|
spacing.dart
|
||||||
|
typography.dart
|
||||||
|
theme_factory.dart
|
||||||
|
user/
|
||||||
|
account_state.dart
|
||||||
|
account_refresh_use_case.dart
|
||||||
|
device/
|
||||||
|
memory_profile.dart
|
||||||
|
|
||||||
|
modules/
|
||||||
|
home/
|
||||||
|
domain/
|
||||||
|
entities/
|
||||||
|
policies/
|
||||||
|
data/
|
||||||
|
home_catalog_api.dart
|
||||||
|
mappers/
|
||||||
|
application/
|
||||||
|
home_catalog_controller.dart
|
||||||
|
playback_visibility_coordinator.dart
|
||||||
|
presentation/
|
||||||
|
home_view.dart
|
||||||
|
components/
|
||||||
|
|
||||||
|
gallery/
|
||||||
|
domain/
|
||||||
|
data/
|
||||||
|
application/
|
||||||
|
presentation/
|
||||||
|
|
||||||
|
creation/
|
||||||
|
domain/
|
||||||
|
data/
|
||||||
|
application/
|
||||||
|
presentation/
|
||||||
|
generate_view.dart
|
||||||
|
progress_view.dart
|
||||||
|
result_view.dart
|
||||||
|
camera_view.dart
|
||||||
|
components/
|
||||||
|
|
||||||
|
billing/
|
||||||
|
domain/
|
||||||
|
data/
|
||||||
|
application/
|
||||||
|
presentation/
|
||||||
|
recharge_view.dart
|
||||||
|
points_history_view.dart
|
||||||
|
payment_webview_view.dart
|
||||||
|
|
||||||
|
profile/
|
||||||
|
domain/
|
||||||
|
data/
|
||||||
|
application/
|
||||||
|
presentation/
|
||||||
|
|
||||||
|
ui_kit/
|
||||||
|
navigation/
|
||||||
|
media/
|
||||||
|
badges/
|
||||||
|
tabs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Mapping from current structure:
|
||||||
|
|
||||||
|
| Current | Target |
|
||||||
|
| --- | --- |
|
||||||
|
| `core/api/*` | `foundation/network/*` and module-specific `data/*_api.dart` |
|
||||||
|
| `core/auth/*` | `foundation/auth/*` |
|
||||||
|
| `core/referrer/*` | `foundation/referrer/*` |
|
||||||
|
| `core/adjust/*` | `foundation/analytics/*` |
|
||||||
|
| `core/theme/*` | `foundation/theme/*` |
|
||||||
|
| `core/user/*` | `foundation/user/*` |
|
||||||
|
| `shared/widgets/*` | `ui_kit/*` or module `presentation/components/*` |
|
||||||
|
| `features/home/*` | `modules/home/{domain,data,application,presentation}` |
|
||||||
|
| `features/generate_video/*` | `modules/creation/{domain,data,application,presentation}` |
|
||||||
|
| `features/recharge/*` | `modules/billing/{domain,data,application,presentation}` |
|
||||||
|
| `features/gallery/*` | `modules/gallery/{domain,data,application,presentation}` |
|
||||||
|
| `features/profile/*` | `modules/profile/{domain,data,application,presentation}` |
|
||||||
|
| `app.dart` | `app_shell/pets_hero_app.dart` |
|
||||||
|
| `main.dart` | `bootstrap/main_bootstrap.dart` plus thin `main.dart` |
|
||||||
|
|
||||||
|
## Architecture Choice Gate
|
||||||
|
|
||||||
|
Before implementation, choose one final target:
|
||||||
|
|
||||||
|
1. **Recommended: Clean Architecture-lite + Vertical Modules**
|
||||||
|
- Best fit because current project is already feature-first.
|
||||||
|
- Produces a clearly different layout while avoiding heavy dependency injection frameworks.
|
||||||
|
2. **Strict Clean Architecture**
|
||||||
|
- Adds explicit repository interfaces and use cases for every module.
|
||||||
|
- More ceremony, higher migration cost.
|
||||||
|
3. **MVVM + Repository**
|
||||||
|
- Good if UI state should be centralized in view models.
|
||||||
|
- Useful if future code will replace large `StatefulWidget` files with controllers.
|
||||||
|
4. **Custom**
|
||||||
|
- Must still define boundaries for app shell, foundation, modules, and UI kit before code is moved.
|
||||||
|
|
||||||
|
If uncertain, use option 1.
|
||||||
|
|
||||||
|
## Non-Negotiable Behavior Invariants
|
||||||
|
|
||||||
|
Preserve these exactly:
|
||||||
|
|
||||||
|
- Startup order:
|
||||||
|
- `ensureDeviceMemoryProfileInitialized`
|
||||||
|
- Adjust initialization
|
||||||
|
- first frame and native splash removal
|
||||||
|
- `AuthService` equivalent login bootstrap
|
||||||
|
- Android Google Play pending purchase listener
|
||||||
|
- order recovery after successful login
|
||||||
|
- API transport:
|
||||||
|
- encrypted proxy request wrapper
|
||||||
|
- V2 `sanctum` payload structure
|
||||||
|
- existing path, method, query, body semantics
|
||||||
|
- timeout and error behavior
|
||||||
|
- Authentication:
|
||||||
|
- device ID selection
|
||||||
|
- MD5 sign calculation
|
||||||
|
- local token restore/write
|
||||||
|
- fast login retry and failure telemetry
|
||||||
|
- Attribution:
|
||||||
|
- Adjust init and attribution callback
|
||||||
|
- Play Install Referrer fallback
|
||||||
|
- organic polling behavior
|
||||||
|
- post-login referrer reporting
|
||||||
|
- `common_info` refresh and home reload behavior
|
||||||
|
- Analytics:
|
||||||
|
- Adjust event tokens
|
||||||
|
- Facebook App Events names and parameters
|
||||||
|
- manual `activateApp`
|
||||||
|
- Facebook app install check log
|
||||||
|
- UI:
|
||||||
|
- same routes
|
||||||
|
- same navigation tabs
|
||||||
|
- same home/category/task loading behavior
|
||||||
|
- same recharge, gallery, generation, and profile behavior
|
||||||
|
- Payments:
|
||||||
|
- Google Play purchase stream subscription timing
|
||||||
|
- pending order recovery
|
||||||
|
- verification payloads
|
||||||
|
- webview payment polling behavior
|
||||||
|
|
||||||
|
## Call Sequence Invariants
|
||||||
|
|
||||||
|
Directory moves, class renames, and file splitting must not change async ordering. Treat the following sequences as contract tests.
|
||||||
|
|
||||||
|
### Cold Startup Sequence
|
||||||
|
|
||||||
|
Current order to preserve:
|
||||||
|
|
||||||
|
1. `WidgetsFlutterBinding.ensureInitialized()`.
|
||||||
|
2. Device memory profile initialization.
|
||||||
|
3. Adjust SDK initialization and attribution callback registration.
|
||||||
|
4. System UI overlay style setup.
|
||||||
|
5. `runApp(...)`.
|
||||||
|
6. Authentication bootstrap starts after `runApp`.
|
||||||
|
7. Android-only pending Google Play purchase listener starts immediately after auth bootstrap is kicked off.
|
||||||
|
8. Order recovery waits for login completion and only runs when startup succeeded.
|
||||||
|
9. First frame removes native splash, logs Facebook app install state, then reports Facebook `activateApp`.
|
||||||
|
|
||||||
|
Do not move auth bootstrap before `runApp`, do not delay the Google Play listener until after login, and do not run order recovery before login completion.
|
||||||
|
|
||||||
|
### Login and Initial API Sequence
|
||||||
|
|
||||||
|
Current order to preserve:
|
||||||
|
|
||||||
|
1. Short startup delay.
|
||||||
|
2. Resolve device ID.
|
||||||
|
3. Compute MD5 sign from device ID.
|
||||||
|
4. Resolve first referrer digest:
|
||||||
|
- race Adjust attribution timeout/callback;
|
||||||
|
- fallback to Play Install Referrer on Android.
|
||||||
|
5. Restore local auth token into the API client.
|
||||||
|
6. Call `POST /v1/user/fast_login` with:
|
||||||
|
- `origin`;
|
||||||
|
- `resolution`;
|
||||||
|
- `digest`.
|
||||||
|
7. On success:
|
||||||
|
- persist returned user token if present;
|
||||||
|
- send register events only when first-register flag is true;
|
||||||
|
- update credits/user/country state;
|
||||||
|
- start post-login referrer/common-info work in the background;
|
||||||
|
- mark startup succeeded.
|
||||||
|
8. On failure:
|
||||||
|
- mark startup failed;
|
||||||
|
- emit `LoginFaild` Facebook event once from the startup failure path.
|
||||||
|
|
||||||
|
Do not call authenticated business APIs before token restore and `fast_login` handling have reached the same point as before.
|
||||||
|
|
||||||
|
### Post-Login Referrer and Common Info Sequence
|
||||||
|
|
||||||
|
Current order to preserve:
|
||||||
|
|
||||||
|
1. Call `GET/SDK Adjust.getAttribution()` equivalent and build the Adjust digest.
|
||||||
|
2. Read Play Install Referrer digest.
|
||||||
|
3. Call `POST /v1/user/referrer` with `accolade=android_adjust`.
|
||||||
|
4. Call `POST /v1/user/referrer` with `accolade=gg`.
|
||||||
|
5. Call `GET /v1/user/common_info`.
|
||||||
|
6. Apply `common_info` fields into user/global state.
|
||||||
|
7. If home structure changed, request full home reload.
|
||||||
|
8. Start organic attribution polling only after this first post-login sequence has completed.
|
||||||
|
|
||||||
|
Organic polling end behavior to preserve:
|
||||||
|
|
||||||
|
1. Poll every 5 seconds.
|
||||||
|
2. Stop when attribution becomes non-organic or 5 minutes elapsed.
|
||||||
|
3. After polling stops, always rerun:
|
||||||
|
- `POST /v1/user/referrer` with `accolade=android_adjust`;
|
||||||
|
- `POST /v1/user/referrer` with `accolade=gg`;
|
||||||
|
- `GET /v1/user/common_info`.
|
||||||
|
4. Only after the rerun finishes, trigger home full reload so `GET /v1/image/img2video/categories` is requested with the refreshed server-side state.
|
||||||
|
|
||||||
|
Do not make the category refresh race ahead of referrer/common-info reporting.
|
||||||
|
|
||||||
|
### Home Loading Sequence
|
||||||
|
|
||||||
|
Current order to preserve:
|
||||||
|
|
||||||
|
1. Home view may be created before login finishes.
|
||||||
|
2. Category loading waits for login completion.
|
||||||
|
3. Call `GET /v1/image/img2video/categories`.
|
||||||
|
4. If `need_wait == true`, append the local `pets` pseudo-category.
|
||||||
|
5. Select the first category.
|
||||||
|
6. If the selected category is not `pets`, call `GET /v1/image/img2video/tasks`.
|
||||||
|
7. If the selected category is `pets`, use `extConfig.items` and do not call task list for `pets`.
|
||||||
|
8. After category/task updates, refresh playback visibility.
|
||||||
|
|
||||||
|
`UserState.requestHomeFullReload()` or its renamed equivalent must still re-enter this category loading sequence.
|
||||||
|
|
||||||
|
### Payment Sequence
|
||||||
|
|
||||||
|
Current order to preserve:
|
||||||
|
|
||||||
|
1. Android purchase stream listener starts early during app startup.
|
||||||
|
2. Google Play order recovery runs after login completion and only after successful startup.
|
||||||
|
3. Recharge purchase actions still emit tier/purchase events at the same user action points.
|
||||||
|
4. Successful purchase verification updates account state before user-visible balance-dependent UI relies on it.
|
||||||
|
5. Purchase success/failure Adjust and Facebook events keep the same event names/tokens and relative timing.
|
||||||
|
6. Third-party payment webview polling keeps the same retry intervals and terminal status handling.
|
||||||
|
|
||||||
|
### Navigation and Lifecycle Sequence
|
||||||
|
|
||||||
|
Current order to preserve:
|
||||||
|
|
||||||
|
1. Route names remain stable unless every navigation caller and documentation reference is migrated together.
|
||||||
|
2. Returning to home still resumes playback through the home playback nonce flow.
|
||||||
|
3. Foreground resume still reports Facebook `activateApp`.
|
||||||
|
4. Media picker flows still temporarily disable screen protection and restore it afterward.
|
||||||
|
|
||||||
|
## Refactor Rules
|
||||||
|
|
||||||
|
### Allowed
|
||||||
|
|
||||||
|
- Move files to new directories.
|
||||||
|
- Split large files by concern.
|
||||||
|
- Rename private classes, helpers, and variables when imports are updated.
|
||||||
|
- Rename public widget classes if all route references and imports are updated in the same step.
|
||||||
|
- Introduce barrel files only when they reduce noisy imports.
|
||||||
|
- Extract controllers/coordinators from large `StatefulWidget` files.
|
||||||
|
- Replace direct helper calls with small application-layer coordinator classes when behavior is unchanged.
|
||||||
|
|
||||||
|
### Not Allowed
|
||||||
|
|
||||||
|
- Changing endpoint paths, request parameters, event names, token keys, or analytics event tokens.
|
||||||
|
- Changing local storage keys unless a migration is added.
|
||||||
|
- Changing route names without updating all navigation callers.
|
||||||
|
- Changing product IDs, purchase completion behavior, or order recovery timing.
|
||||||
|
- Changing image/video playback rules while moving code.
|
||||||
|
- Adding new state-management packages only for the refactor.
|
||||||
|
- Reformatting or rewriting generated/platform files unrelated to the migration.
|
||||||
|
|
||||||
|
## File Splitting Targets
|
||||||
|
|
||||||
|
Prioritize these files because they currently carry several responsibilities:
|
||||||
|
|
||||||
|
- `lib/app.dart`
|
||||||
|
- App widget, route table, lifecycle reporting, startup overlay, main scaffold.
|
||||||
|
- Split into app shell, routes, lifecycle reporter, startup overlay, main scaffold.
|
||||||
|
- `lib/core/auth/auth_service.dart`
|
||||||
|
- Device identity, token restore, login retry, common info, referrer reporting, screen security.
|
||||||
|
- Split into session bootstrapper, device identity provider, token vault, common info synchronizer, referrer post-login coordinator.
|
||||||
|
- `lib/core/referrer/referrer_service.dart`
|
||||||
|
- Adjust digest, Play referrer, organic polling.
|
||||||
|
- Split into attribution digest builder, install referrer probe, attribution polling coordinator.
|
||||||
|
- `lib/features/home/home_screen.dart`
|
||||||
|
- Category loading, task loading, ext config parsing, playback visibility, UI layout.
|
||||||
|
- Split into home controller, catalog data source, playback visibility coordinator, home view components.
|
||||||
|
- `lib/features/generate_video/generate_video_screen.dart`
|
||||||
|
- UI, upload, creation request, media selection, camera/album coordination.
|
||||||
|
- Split into creation flow controller, upload coordinator, prompt/media components.
|
||||||
|
- `lib/features/recharge/recharge_screen.dart`
|
||||||
|
- Product rendering, purchase flow, third-party payment, event reporting.
|
||||||
|
- Split into billing controller, purchase coordinator, payment option components.
|
||||||
|
|
||||||
|
## Suggested Migration Order
|
||||||
|
|
||||||
|
Work in small batches. After each batch, run analysis and at least one release/debug build smoke check.
|
||||||
|
|
||||||
|
### Phase 0: Baseline
|
||||||
|
|
||||||
|
Capture current behavior before moving code:
|
||||||
|
|
||||||
|
- Run `flutter analyze`.
|
||||||
|
- Run the app with trace logs:
|
||||||
|
- `flutter run --release --dart-define=APP_LOG_LEVEL=trace`
|
||||||
|
- Verify logs for:
|
||||||
|
- Adjust init and attribution callback.
|
||||||
|
- Facebook install check.
|
||||||
|
- fast login.
|
||||||
|
- referrer reporting.
|
||||||
|
- `common_info`.
|
||||||
|
- categories and first task load.
|
||||||
|
- purchase listener startup on Android.
|
||||||
|
|
||||||
|
### Phase 1: App Shell
|
||||||
|
|
||||||
|
Move without changing behavior:
|
||||||
|
|
||||||
|
- `main.dart` becomes a thin entrypoint.
|
||||||
|
- App widget moves to `app_shell/pets_hero_app.dart`.
|
||||||
|
- Route definitions move to `app_shell/routes/route_table.dart`.
|
||||||
|
- Route observer moves to `app_shell/routes/route_observer.dart`.
|
||||||
|
- Startup overlay moves to `app_shell/lifecycle/startup_overlay.dart`.
|
||||||
|
- Facebook lifecycle/install-check logic moves to `app_shell/lifecycle/facebook_lifecycle_reporter.dart`.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- App opens to the same tab.
|
||||||
|
- Native splash is removed at the same time.
|
||||||
|
- `activateApp` and install check logs still appear.
|
||||||
|
|
||||||
|
### Phase 2: Foundation Network and Config
|
||||||
|
|
||||||
|
Move:
|
||||||
|
|
||||||
|
- API config, API client, proxy client, crypto, response type.
|
||||||
|
- Logging.
|
||||||
|
- Facebook/Adjust config.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `fast_login`, `common_info`, category/task requests still show identical paths and body shapes in trace logs.
|
||||||
|
- No endpoint path changes.
|
||||||
|
|
||||||
|
### Phase 3: Foundation Auth and Attribution
|
||||||
|
|
||||||
|
Move and split:
|
||||||
|
|
||||||
|
- Auth/session bootstrapper.
|
||||||
|
- Device identity and sign generation.
|
||||||
|
- Token vault.
|
||||||
|
- Referrer digest and polling.
|
||||||
|
- Adjust/Facebook event gateways.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- New install and returning-user startup both work.
|
||||||
|
- Natural/organic attribution polling still runs every 5s and stops after 5min or non-organic attribution.
|
||||||
|
- After polling stops, referrer is reported, `common_info` refreshes, then categories reload.
|
||||||
|
|
||||||
|
### Phase 4: UI Kit
|
||||||
|
|
||||||
|
Move shared visual widgets:
|
||||||
|
|
||||||
|
- Bottom navigation.
|
||||||
|
- Top navigation.
|
||||||
|
- Credits badge.
|
||||||
|
- Tab selector scope or replacement shell coordinator.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Bottom tabs, top bar, credits display, and route behavior are unchanged.
|
||||||
|
|
||||||
|
### Phase 5: Home Module
|
||||||
|
|
||||||
|
Move and split:
|
||||||
|
|
||||||
|
- Entities:
|
||||||
|
- category item
|
||||||
|
- task item
|
||||||
|
- ext config item
|
||||||
|
- Data:
|
||||||
|
- category/task API wrappers
|
||||||
|
- JSON mappers
|
||||||
|
- Application:
|
||||||
|
- category/task loading coordinator
|
||||||
|
- ext config policy
|
||||||
|
- playback visibility coordinator
|
||||||
|
- Presentation:
|
||||||
|
- home view
|
||||||
|
- tab row
|
||||||
|
- video card
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `need_wait` behavior unchanged.
|
||||||
|
- `pets` pseudo-category behavior unchanged.
|
||||||
|
- Video autoplay visibility behavior unchanged.
|
||||||
|
- `UserState.requestHomeFullReload()` still reloads categories and tasks.
|
||||||
|
|
||||||
|
### Phase 6: Creation Module
|
||||||
|
|
||||||
|
Move:
|
||||||
|
|
||||||
|
- Generate screen.
|
||||||
|
- Progress screen.
|
||||||
|
- Result screen.
|
||||||
|
- In-app camera page.
|
||||||
|
- Album picker.
|
||||||
|
- Image compression helpers if only used by creation.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Album/camera selection works.
|
||||||
|
- Upload and task creation request shapes are unchanged.
|
||||||
|
- Progress polling and result navigation are unchanged.
|
||||||
|
|
||||||
|
### Phase 7: Gallery Module
|
||||||
|
|
||||||
|
Move:
|
||||||
|
|
||||||
|
- Gallery screen.
|
||||||
|
- Gallery task item.
|
||||||
|
- Upload cover store.
|
||||||
|
- Thumbnail cache.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Gallery loads the same data.
|
||||||
|
- Video thumbnails/cache behavior unchanged.
|
||||||
|
- Navigation to result/generate flows unchanged.
|
||||||
|
|
||||||
|
### Phase 8: Billing Module
|
||||||
|
|
||||||
|
Move:
|
||||||
|
|
||||||
|
- Recharge screen.
|
||||||
|
- Payment webview.
|
||||||
|
- Points history.
|
||||||
|
- Google Play purchase service.
|
||||||
|
- Payment models.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Products render the same.
|
||||||
|
- Google Play purchase and pending purchase recovery work.
|
||||||
|
- Third-party payment webview flow works.
|
||||||
|
- Purchase success/failure Adjust and Facebook events are unchanged.
|
||||||
|
|
||||||
|
### Phase 9: Profile Module
|
||||||
|
|
||||||
|
Move:
|
||||||
|
|
||||||
|
- Profile screen.
|
||||||
|
- Account deletion / account display dependencies if any.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- Profile data, recharge navigation, account actions are unchanged.
|
||||||
|
|
||||||
|
### Phase 10: Cleanup
|
||||||
|
|
||||||
|
- Remove old empty directories.
|
||||||
|
- Remove transitional imports.
|
||||||
|
- Keep barrel files minimal.
|
||||||
|
- Update docs that reference old paths:
|
||||||
|
- `docs/app_startup.md`
|
||||||
|
- `docs/home.md`
|
||||||
|
- `docs/payment_flow.md`
|
||||||
|
- `docs/generate_video.md`
|
||||||
|
- `docs/user_login.md`
|
||||||
|
|
||||||
|
## Import Strategy
|
||||||
|
|
||||||
|
Use package imports for cross-module references:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:pets_hero_ai/foundation/network/api_response.dart';
|
||||||
|
import 'package:pets_hero_ai/modules/home/presentation/home_view.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid long chains of relative imports across modules.
|
||||||
|
|
||||||
|
Optional barrel files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
foundation/network/network.dart
|
||||||
|
modules/home/home.dart
|
||||||
|
modules/billing/billing.dart
|
||||||
|
ui_kit/ui_kit.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a barrel only when at least three external files import the same group.
|
||||||
|
|
||||||
|
## Naming Style
|
||||||
|
|
||||||
|
Use a noticeably different naming style while keeping semantics clear:
|
||||||
|
|
||||||
|
- `AuthService` → `SessionBootstrapper`
|
||||||
|
- `ReferrerService` → `AttributionCoordinator`
|
||||||
|
- `ImageApi` category/task calls → `HomeCatalogApi`
|
||||||
|
- `UserState` → `AccountState`
|
||||||
|
- `AdjustEvents` → `MarketingEventGateway`
|
||||||
|
- `HomeScreen` → `HomeView`
|
||||||
|
- `GenerateVideoScreen` → `CreationView`
|
||||||
|
- `RechargeScreen` → `BillingView`
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
- `Coordinator` for orchestration.
|
||||||
|
- `Gateway` for third-party SDK/event wrappers.
|
||||||
|
- `Probe` for platform capability checks.
|
||||||
|
- `Vault` for local secure-ish persistence.
|
||||||
|
- `Policy` for pure decision logic.
|
||||||
|
- `Mapper` for JSON conversion.
|
||||||
|
|
||||||
|
## State Management Guidance
|
||||||
|
|
||||||
|
Do not add Bloc/Riverpod/Provider just to make the rewrite look different.
|
||||||
|
|
||||||
|
For this project, prefer lightweight application-layer controllers:
|
||||||
|
|
||||||
|
- Keep existing `ValueNotifier`-based global state where behavior depends on it.
|
||||||
|
- Extract complex `StatefulWidget` logic into controllers that are owned by the widget state.
|
||||||
|
- Use `ListenableBuilder`/`ValueListenableBuilder` where the existing behavior already relies on notifiers.
|
||||||
|
- Only replace `setState` when the extracted controller makes ownership clearer.
|
||||||
|
|
||||||
|
## Verification Checklist Per Phase
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flutter analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
For Android smoke checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flutter run --release --dart-define=APP_LOG_LEVEL=trace
|
||||||
|
```
|
||||||
|
|
||||||
|
Check logs for changed phases:
|
||||||
|
|
||||||
|
- Request paths and query/body fields.
|
||||||
|
- Auth/referrer/common_info sequence.
|
||||||
|
- Adjust/Facebook events.
|
||||||
|
- Home category/task reloads.
|
||||||
|
- Purchase listener and recovery startup.
|
||||||
|
|
||||||
|
For every phase that touches startup, auth, referrer, home, or billing, compare the trace log against a pre-refactor baseline. The comparison must verify order, not only presence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Adjust init
|
||||||
|
runApp
|
||||||
|
Auth bootstrap
|
||||||
|
Google Play listener
|
||||||
|
fast_login
|
||||||
|
post-login referrer(android_adjust)
|
||||||
|
post-login referrer(gg)
|
||||||
|
common_info
|
||||||
|
categories
|
||||||
|
tasks, when applicable
|
||||||
|
organic polling stop
|
||||||
|
referrer(android_adjust)
|
||||||
|
referrer(gg)
|
||||||
|
common_info
|
||||||
|
categories
|
||||||
|
```
|
||||||
|
|
||||||
|
If any request appears earlier/later than the baseline, stop the migration and decide explicitly whether the order change is acceptable. The default answer is no.
|
||||||
|
|
||||||
|
Manual UI checks:
|
||||||
|
|
||||||
|
- App launch.
|
||||||
|
- Home list and tabs.
|
||||||
|
- Generate flow.
|
||||||
|
- Gallery flow.
|
||||||
|
- Recharge flow.
|
||||||
|
- Profile flow.
|
||||||
|
|
||||||
|
## Recommended Work Protocol
|
||||||
|
|
||||||
|
1. Move one module or one foundation slice at a time.
|
||||||
|
2. Keep old behavior tests/manual traces open while migrating.
|
||||||
|
3. Update imports immediately after every move.
|
||||||
|
4. Run analyzer before continuing to the next phase.
|
||||||
|
5. Do not combine architecture moves with feature fixes.
|
||||||
|
6. If a behavior bug is discovered, document it separately and decide whether to fix before or after migration.
|
||||||
|
7. Commit after each compiling phase if using git commits.
|
||||||
|
|
||||||
|
## Completion Criteria
|
||||||
|
|
||||||
|
The refactor is complete only when:
|
||||||
|
|
||||||
|
- No imports reference the old `core/`, `features/`, or `shared/` directories unless those directories intentionally remain as compatibility shims.
|
||||||
|
- The app compiles cleanly.
|
||||||
|
- Startup, login, attribution, home reload, generation, gallery, recharge, and profile flows match baseline behavior.
|
||||||
|
- Trace logs show the same API paths and event names as before.
|
||||||
|
- Old large files have been split along the target architecture boundaries.
|
||||||
|
- Docs and any route/import examples use the new paths.
|
||||||
|
|
||||||
@ -26,6 +26,10 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>FacebookAutoLogAppEventsEnabled</key>
|
<key>FacebookAutoLogAppEventsEnabled</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>fb</string>
|
||||||
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
|
|||||||
18
lib/app.dart
18
lib/app.dart
@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:facebook_app_events/facebook_app_events.dart';
|
import 'package:facebook_app_events/facebook_app_events.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'core/auth/auth_service.dart';
|
import 'core/auth/auth_service.dart';
|
||||||
import 'core/nav/app_route_observer.dart';
|
import 'core/nav/app_route_observer.dart';
|
||||||
@ -46,6 +47,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// Remove native splash; Flutter login overlay takes over if startup is still running.
|
// Remove native splash; Flutter login overlay takes over if startup is still running.
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
|
unawaited(_logFacebookInstallState('first_frame'));
|
||||||
_reportFacebookActivateApp('first_frame');
|
_reportFacebookActivateApp('first_frame');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -65,6 +67,22 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect whether a local app can handle Facebook's URL scheme.
|
||||||
|
///
|
||||||
|
/// iOS requires `LSApplicationQueriesSchemes` to include `fb`; Android 11+
|
||||||
|
/// requires a matching `<queries>` entry in AndroidManifest.
|
||||||
|
Future<void> _logFacebookInstallState(String reason) async {
|
||||||
|
const scheme = 'fb://';
|
||||||
|
try {
|
||||||
|
final installed = await canLaunchUrl(Uri.parse(scheme));
|
||||||
|
_fbLog.d(
|
||||||
|
'Facebook app install check ($reason): installed=$installed scheme=$scheme',
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
_fbLog.e('Facebook app install check failed ($reason)', e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Foreground resume; cold start also uses addPostFrameCallback in initState.
|
/// Foreground resume; cold start also uses addPostFrameCallback in initState.
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
|||||||
@ -553,6 +553,20 @@ class AuthService {
|
|||||||
static Future<void> _runPostLoginReferrerWork(String uid, String deviceId) async {
|
static Future<void> _runPostLoginReferrerWork(String uid, String deviceId) async {
|
||||||
try {
|
try {
|
||||||
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
|
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
|
||||||
|
// 首次归因可能是“延迟到的自然量”:开启 5s 轮询、最长 5min。
|
||||||
|
// 轮询结束后,无论是否检测到非自然量都重新上报归因 + 刷新 common_info,
|
||||||
|
// 再触发首页完整重载(_loadCategories 会重拉分类数据)。
|
||||||
|
unawaited(ReferrerService.startOrganicPolling(
|
||||||
|
onStopped: (detectedNonOrganic) async {
|
||||||
|
try {
|
||||||
|
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
|
||||||
|
} catch (e, st) {
|
||||||
|
_logMsg('organic poll: 重新上报归因/common_info 异常: $e');
|
||||||
|
_logMsg('organic poll: 堆栈 $st');
|
||||||
|
}
|
||||||
|
UserState.requestHomeFullReload();
|
||||||
|
},
|
||||||
|
));
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
_logMsg('referrer/common_info 后台任务异常: $e');
|
_logMsg('referrer/common_info 后台任务异常: $e');
|
||||||
_logMsg('referrer/common_info 堆栈: $st');
|
_logMsg('referrer/common_info 堆栈: $st');
|
||||||
|
|||||||
@ -6,10 +6,14 @@ import 'package:adjust_sdk/adjust.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:play_install_referrer/play_install_referrer.dart';
|
import 'package:play_install_referrer/play_install_referrer.dart';
|
||||||
|
|
||||||
|
import '../log/app_logger.dart';
|
||||||
|
|
||||||
/// 归因信息服务(优先从 Adjust 获取,fallback 使用 Play Install Referrer)
|
/// 归因信息服务(优先从 Adjust 获取,fallback 使用 Play Install Referrer)
|
||||||
class ReferrerService {
|
class ReferrerService {
|
||||||
ReferrerService._();
|
ReferrerService._();
|
||||||
|
|
||||||
|
static final _log = AppLogger('ReferrerService');
|
||||||
|
|
||||||
static String? _cachedReferrer;
|
static String? _cachedReferrer;
|
||||||
static String _referrerSource = 'gg';
|
static String _referrerSource = 'gg';
|
||||||
static final Completer<String?> _completer = Completer<String?>();
|
static final Completer<String?> _completer = Completer<String?>();
|
||||||
@ -27,6 +31,15 @@ class ReferrerService {
|
|||||||
/// Play Install Referrer 读取超时(部分机型/系统上可能极慢)
|
/// Play Install Referrer 读取超时(部分机型/系统上可能极慢)
|
||||||
static const Duration _playInstallReferrerTimeout = Duration(seconds: 10);
|
static const Duration _playInstallReferrerTimeout = Duration(seconds: 10);
|
||||||
|
|
||||||
|
/// 自然量轮询:每次检查间隔
|
||||||
|
static const Duration _organicPollInterval = Duration(seconds: 5);
|
||||||
|
|
||||||
|
/// 自然量轮询:最长持续时间(达到后无论是否仍是自然量都停止)
|
||||||
|
static const Duration _organicPollMaxDuration = Duration(minutes: 5);
|
||||||
|
|
||||||
|
static bool _organicPollRunning = false;
|
||||||
|
static Timer? _organicPollTimer;
|
||||||
|
|
||||||
/// 由 Adjust attributionCallback 调用,首次安装时归因往往通过此回调返回(晚于 getAttributionWithTimeout)
|
/// 由 Adjust attributionCallback 调用,首次安装时归因往往通过此回调返回(晚于 getAttributionWithTimeout)
|
||||||
static void receiveAttributionFromCallback(AdjustAttribution attribution) {
|
static void receiveAttributionFromCallback(AdjustAttribution attribution) {
|
||||||
if (!_attributionCallbackCompleter.isCompleted) {
|
if (!_attributionCallbackCompleter.isCompleted) {
|
||||||
@ -143,4 +156,133 @@ class ReferrerService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 启动“自然量→渠道”轮询:仅当 Adjust 与 Play Install Referrer 当前都判定为自然量时开始;
|
||||||
|
/// 之后每 [_organicPollInterval] 重新判定一次,直到任一渠道变为非自然量或超过
|
||||||
|
/// [_organicPollMaxDuration]。结束时调用 [onStopped],参数表示是否检测到非自然量。
|
||||||
|
///
|
||||||
|
/// 多次调用是幂等的:已在轮询时直接返回。
|
||||||
|
static Future<void> startOrganicPolling({
|
||||||
|
required Future<void> Function(bool detectedNonOrganic) onStopped,
|
||||||
|
}) async {
|
||||||
|
if (_organicPollRunning) {
|
||||||
|
_log.d('startOrganicPolling: already running, skip');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final initiallyOrganic = await _isCurrentlyOrganic();
|
||||||
|
if (!initiallyOrganic) {
|
||||||
|
_log.d('startOrganicPolling: 初始归因即为渠道,无需轮询');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_organicPollRunning = true;
|
||||||
|
final startedAt = DateTime.now();
|
||||||
|
var tickCount = 0;
|
||||||
|
_log.d(
|
||||||
|
'startOrganicPolling: 启动自然量轮询,每 ${_organicPollInterval.inSeconds}s 一次,最长 ${_organicPollMaxDuration.inMinutes}min');
|
||||||
|
|
||||||
|
Future<void> finish(bool detectedNonOrganic, {required String reason}) async {
|
||||||
|
_stopOrganicPoll();
|
||||||
|
_log.d('startOrganicPolling: 结束,原因=$reason changed=$detectedNonOrganic');
|
||||||
|
try {
|
||||||
|
await onStopped(detectedNonOrganic);
|
||||||
|
} catch (e, st) {
|
||||||
|
_log.d('startOrganicPolling: onStopped 回调异常: $e');
|
||||||
|
_log.d('startOrganicPolling: stack $st');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tick() async {
|
||||||
|
if (!_organicPollRunning) return;
|
||||||
|
|
||||||
|
tickCount++;
|
||||||
|
final elapsed = DateTime.now().difference(startedAt);
|
||||||
|
if (elapsed >= _organicPollMaxDuration) {
|
||||||
|
await finish(false, reason: 'timeout');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final adjustNetwork = await _readAdjustNetwork();
|
||||||
|
final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork);
|
||||||
|
final pirRef = await _readPlayInstallReferrerRaw();
|
||||||
|
final pirOrganic = _installReferrerIsOrganic(pirRef);
|
||||||
|
final stillOrganic = adjustOrganic && pirOrganic;
|
||||||
|
_log.d(
|
||||||
|
'organic poll tick#$tickCount elapsed=${elapsed.inSeconds}s '
|
||||||
|
'adjustNetwork="${adjustNetwork ?? '<null>'}" adjustOrganic=$adjustOrganic '
|
||||||
|
'pir="$pirRef" pirOrganic=$pirOrganic stillOrganic=$stillOrganic');
|
||||||
|
|
||||||
|
if (!_organicPollRunning) return;
|
||||||
|
|
||||||
|
if (!stillOrganic) {
|
||||||
|
await finish(true, reason: 'detectedNonOrganic');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_organicPollTimer = Timer(_organicPollInterval, () {
|
||||||
|
if (!_organicPollRunning) return;
|
||||||
|
unawaited(tick());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_organicPollTimer = Timer(_organicPollInterval, () {
|
||||||
|
if (!_organicPollRunning) return;
|
||||||
|
unawaited(tick());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _stopOrganicPoll() {
|
||||||
|
_organicPollTimer?.cancel();
|
||||||
|
_organicPollTimer = null;
|
||||||
|
_organicPollRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当 Adjust 归因和 Play Install Referrer 都判定为自然量时返回 true
|
||||||
|
static Future<bool> _isCurrentlyOrganic() async {
|
||||||
|
final adjustNetwork = await _readAdjustNetwork();
|
||||||
|
final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork);
|
||||||
|
final pirRef = await _readPlayInstallReferrerRaw();
|
||||||
|
final pirOrganic = _installReferrerIsOrganic(pirRef);
|
||||||
|
return adjustOrganic && pirOrganic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取 Adjust 当前归因的 network 字段(异常返回 null,按未归因处理)
|
||||||
|
static Future<String?> _readAdjustNetwork() async {
|
||||||
|
try {
|
||||||
|
final attr = await Adjust.getAttribution();
|
||||||
|
return attr.network;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// network 为空 / 'organic' 视为自然量(含未归因情况)
|
||||||
|
static bool _attributionIsOrganicByNetwork(String? network) {
|
||||||
|
final n = (network ?? '').trim().toLowerCase();
|
||||||
|
return n.isEmpty || n == 'organic';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取 Play Install Referrer 原始字符串(非 Android / 异常返回空串)
|
||||||
|
static Future<String> _readPlayInstallReferrerRaw() async {
|
||||||
|
if (defaultTargetPlatform != TargetPlatform.android) return '';
|
||||||
|
try {
|
||||||
|
final details = await PlayInstallReferrer.installReferrer
|
||||||
|
.timeout(_playInstallReferrerTimeout);
|
||||||
|
return details.installReferrer ?? '';
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Google Play 自然量典型 referrer:`utm_source=google-play&utm_medium=organic`;
|
||||||
|
/// 空串/缺少 `utm_medium=` 视为尚未归因,按自然量处理。
|
||||||
|
static bool _installReferrerIsOrganic(String ref) {
|
||||||
|
final s = ref.trim();
|
||||||
|
if (s.isEmpty) return true;
|
||||||
|
final lower = s.toLowerCase();
|
||||||
|
if (lower.contains('utm_medium=organic')) return true;
|
||||||
|
if (!lower.contains('utm_medium=')) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
name: pets_hero_ai
|
name: pets_hero_ai
|
||||||
description: PetsHero AI Application.
|
description: PetsHero AI Application.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.2.0+120
|
version: 1.2.1+121
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user