Skip to content

Commit 2a2e1bc

Browse files
committed
feat(example): add precomputed client demo
Add demonstration of the precomputed client in the example app: - Rename MainActivity -> HomeActivity, SecondActivity -> StandardClientActivity - Add PrecomputedActivity with: - Subject ID input with server/disk initialization options - Dynamic subject attributes table (add/remove key-value pairs) - Flag key input with type selection (string, bool, int, numeric, JSON) - Assignment log display - Proper lifecycle handling for polling (pause/resume/stop) - Add back navigation to all activities - Update layouts and strings This provides a complete interactive demo of the precomputed client for testing and development purposes.
1 parent fa04c22 commit 2a2e1bc

7 files changed

Lines changed: 525 additions & 10 deletions

File tree

example/src/main/AndroidManifest.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
android:theme="@style/Theme.EppoExample"
1515
tools:targetApi="30">
1616
<activity
17-
android:name="cloud.eppo.androidexample.MainActivity"
17+
android:name="cloud.eppo.androidexample.HomeActivity"
1818
android:exported="true">
1919
<intent-filter>
2020
<action android:name="android.intent.action.MAIN" />
@@ -24,7 +24,12 @@
2424
</activity>
2525

2626
<activity
27-
android:name="cloud.eppo.androidexample.SecondActivity" />
27+
android:name="cloud.eppo.androidexample.StandardClientActivity"
28+
android:parentActivityName="cloud.eppo.androidexample.HomeActivity" />
29+
30+
<activity
31+
android:name="cloud.eppo.androidexample.PrecomputedActivity"
32+
android:parentActivityName="cloud.eppo.androidexample.HomeActivity" />
2833
</application>
2934

3035
</manifest>

example/src/main/java/cloud/eppo/androidexample/MainActivity.java renamed to example/src/main/java/cloud/eppo/androidexample/HomeActivity.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@
1414
import cloud.eppo.android.EppoClient;
1515
import com.geteppo.androidexample.BuildConfig;
1616
import com.geteppo.androidexample.R;
17+
import java.io.File;
1718

18-
public class MainActivity extends AppCompatActivity {
19+
public class HomeActivity extends AppCompatActivity {
1920
private static final String API_KEY = BuildConfig.API_KEY; // Set in root-level local.properties
2021

2122
@Override
2223
protected void onCreate(Bundle savedInstanceState) {
2324
super.onCreate(savedInstanceState);
24-
setContentView(R.layout.activity_main);
25+
setContentView(R.layout.activity_home);
2526
Button button = findViewById(R.id.button_start_assigner);
26-
Intent launchAssigner = new Intent(MainActivity.this, SecondActivity.class);
27+
Intent launchAssigner = new Intent(HomeActivity.this, StandardClientActivity.class);
2728

2829
button.setOnClickListener(view -> startActivity(launchAssigner));
2930

@@ -32,15 +33,39 @@ protected void onCreate(Bundle savedInstanceState) {
3233
view ->
3334
startActivity(launchAssigner.putExtra(this.getPackageName() + ".offlineMode", false)));
3435

36+
Button precomputedButton = findViewById(R.id.button_start_precomputed);
37+
Intent launchPrecomputed = new Intent(HomeActivity.this, PrecomputedActivity.class);
38+
precomputedButton.setOnClickListener(view -> startActivity(launchPrecomputed));
39+
3540
Button clearCacheButton = findViewById(R.id.button_clear_cache);
3641
clearCacheButton.setOnClickListener(view -> clearCacheFile());
3742
}
3843

3944
private void clearCacheFile() {
45+
// Clear standard client cache
4046
String cacheFileNameSuffix = safeCacheKey(API_KEY);
4147
ConfigCacheFile cacheFile = new ConfigCacheFile(getApplication(), cacheFileNameSuffix);
4248
cacheFile.delete();
43-
Toast.makeText(this, "Cache Cleared", Toast.LENGTH_SHORT).show();
49+
50+
// Clear all precomputed client caches (they include subject key hash in filename)
51+
File filesDir = getApplication().getFilesDir();
52+
File[] precomputedCaches =
53+
filesDir.listFiles(
54+
(dir, name) -> name.startsWith("eppo-sdk-precomputed-") && name.endsWith(".json"));
55+
int precomputedCount = 0;
56+
if (precomputedCaches != null) {
57+
for (File file : precomputedCaches) {
58+
if (file.delete()) {
59+
precomputedCount++;
60+
}
61+
}
62+
}
63+
64+
String message =
65+
precomputedCount > 0
66+
? "Cache Cleared (" + precomputedCount + " precomputed)"
67+
: "Cache Cleared";
68+
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
4469
}
4570

4671
@Override
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package cloud.eppo.androidexample;
2+
3+
import android.os.Bundle;
4+
import android.text.TextUtils;
5+
import android.util.Log;
6+
import android.view.MenuItem;
7+
import android.view.View;
8+
import android.widget.Button;
9+
import android.widget.EditText;
10+
import android.widget.LinearLayout;
11+
import android.widget.RadioGroup;
12+
import android.widget.ScrollView;
13+
import android.widget.TextView;
14+
import androidx.appcompat.app.AppCompatActivity;
15+
import cloud.eppo.android.EppoPrecomputedClient;
16+
import cloud.eppo.api.Attributes;
17+
import cloud.eppo.api.EppoValue;
18+
import com.geteppo.androidexample.BuildConfig;
19+
import com.geteppo.androidexample.R;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
/**
24+
* Example activity demonstrating the EppoPrecomputedClient. The precomputed client computes all
25+
* flag assignments server-side for a specific subject, providing instant lookups.
26+
*/
27+
public class PrecomputedActivity extends AppCompatActivity {
28+
private static final String TAG = PrecomputedActivity.class.getSimpleName();
29+
private static final String API_KEY = BuildConfig.API_KEY;
30+
31+
private EditText subjectInput;
32+
private EditText flagKeyInput;
33+
private RadioGroup flagTypeGroup;
34+
private TextView assignmentLog;
35+
private ScrollView assignmentLogScrollView;
36+
private TextView statusText;
37+
private LinearLayout attributesContainer;
38+
private Button getAssignmentButton;
39+
private List<View> attributeRows = new ArrayList<>();
40+
41+
private EppoPrecomputedClient precomputedClient;
42+
43+
@Override
44+
protected void onCreate(Bundle savedInstanceState) {
45+
super.onCreate(savedInstanceState);
46+
setContentView(R.layout.activity_precomputed);
47+
48+
// Enable the action bar back button
49+
if (getSupportActionBar() != null) {
50+
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
51+
getSupportActionBar().setTitle("Precomputed Client");
52+
}
53+
54+
subjectInput = findViewById(R.id.precomputed_subject);
55+
flagKeyInput = findViewById(R.id.precomputed_flag_key);
56+
flagTypeGroup = findViewById(R.id.flag_type_group);
57+
assignmentLog = findViewById(R.id.precomputed_assignment_log);
58+
assignmentLogScrollView = findViewById(R.id.precomputed_assignment_log_scrollview);
59+
statusText = findViewById(R.id.precomputed_status);
60+
attributesContainer = findViewById(R.id.attributes_container);
61+
62+
findViewById(R.id.btn_init_server).setOnClickListener(view -> initializeClient(false));
63+
findViewById(R.id.btn_init_disk).setOnClickListener(view -> initializeClient(true));
64+
getAssignmentButton = findViewById(R.id.btn_get_assignment);
65+
getAssignmentButton.setEnabled(false);
66+
getAssignmentButton.setOnClickListener(view -> getAssignment());
67+
findViewById(R.id.btn_add_attribute).setOnClickListener(view -> addAttributeRow("", ""));
68+
69+
// Add default attributes
70+
addAttributeRow("platform", "android");
71+
addAttributeRow("appVersion", BuildConfig.VERSION_NAME);
72+
}
73+
74+
private void addAttributeRow(String key, String value) {
75+
LinearLayout row = new LinearLayout(this);
76+
row.setOrientation(LinearLayout.HORIZONTAL);
77+
row.setLayoutParams(
78+
new LinearLayout.LayoutParams(
79+
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
80+
81+
EditText keyInput = new EditText(this);
82+
keyInput.setHint("Key");
83+
keyInput.setText(key);
84+
LinearLayout.LayoutParams keyParams =
85+
new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
86+
keyInput.setLayoutParams(keyParams);
87+
keyInput.setTag("key");
88+
89+
EditText valueInput = new EditText(this);
90+
valueInput.setHint("Value");
91+
valueInput.setText(value);
92+
LinearLayout.LayoutParams valueParams =
93+
new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
94+
valueParams.setMarginStart(8);
95+
valueInput.setLayoutParams(valueParams);
96+
valueInput.setTag("value");
97+
98+
Button removeButton = new Button(this);
99+
removeButton.setText("X");
100+
removeButton.setMinWidth(0);
101+
removeButton.setMinHeight(0);
102+
removeButton.setMinimumWidth(0);
103+
removeButton.setMinimumHeight(0);
104+
removeButton.setPadding(16, 8, 16, 8);
105+
LinearLayout.LayoutParams removeParams =
106+
new LinearLayout.LayoutParams(
107+
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
108+
removeParams.setMarginStart(8);
109+
removeButton.setLayoutParams(removeParams);
110+
removeButton.setOnClickListener(
111+
v -> {
112+
attributesContainer.removeView(row);
113+
attributeRows.remove(row);
114+
});
115+
116+
row.addView(keyInput);
117+
row.addView(valueInput);
118+
row.addView(removeButton);
119+
120+
attributesContainer.addView(row);
121+
attributeRows.add(row);
122+
}
123+
124+
private Attributes collectAttributes() {
125+
Attributes attributes = new Attributes();
126+
for (View row : attributeRows) {
127+
EditText keyInput = row.findViewWithTag("key");
128+
EditText valueInput = row.findViewWithTag("value");
129+
if (keyInput != null && valueInput != null) {
130+
String key = keyInput.getText().toString().trim();
131+
String value = valueInput.getText().toString().trim();
132+
if (!key.isEmpty()) {
133+
// Try to parse as number first
134+
try {
135+
double numValue = Double.parseDouble(value);
136+
attributes.put(key, EppoValue.valueOf(numValue));
137+
} catch (NumberFormatException e) {
138+
// Use as string
139+
attributes.put(key, EppoValue.valueOf(value));
140+
}
141+
}
142+
}
143+
}
144+
return attributes;
145+
}
146+
147+
private void initializeClient(boolean offlineMode) {
148+
String subjectKey = subjectInput.getText().toString();
149+
if (TextUtils.isEmpty(subjectKey)) {
150+
appendToLog("Subject ID is required");
151+
return;
152+
}
153+
154+
String source = offlineMode ? "disk" : "server";
155+
statusText.setText("Initializing from " + source + "...");
156+
appendToLog(
157+
"Initializing precomputed client for subject: " + subjectKey + " (from " + source + ")");
158+
159+
// Collect subject attributes from the UI
160+
Attributes subjectAttributes = collectAttributes();
161+
appendToLog("Subject attributes: " + subjectAttributes.size() + " attributes");
162+
163+
new EppoPrecomputedClient.Builder(API_KEY, getApplication())
164+
.subjectKey(subjectKey)
165+
.subjectAttributes(subjectAttributes)
166+
.isGracefulMode(true)
167+
.forceReinitialize(true)
168+
.offlineMode(offlineMode)
169+
.assignmentLogger(
170+
assignment -> {
171+
Log.d(
172+
TAG,
173+
"Assignment logged: "
174+
+ assignment.getFeatureFlag()
175+
+ " -> "
176+
+ assignment.getVariation());
177+
})
178+
.buildAndInitAsync()
179+
.thenAccept(
180+
client -> {
181+
precomputedClient = client;
182+
runOnUiThread(
183+
() -> {
184+
statusText.setText("Initialized for: " + subjectKey + " (from " + source + ")");
185+
appendToLog("Client initialized successfully from " + source + "!");
186+
getAssignmentButton.setEnabled(true);
187+
});
188+
})
189+
.exceptionally(
190+
error -> {
191+
Log.e(TAG, "Failed to initialize", error);
192+
runOnUiThread(
193+
() -> {
194+
statusText.setText("Initialization failed");
195+
appendToLog("Error: " + error.getMessage());
196+
});
197+
return null;
198+
});
199+
}
200+
201+
private void getAssignment() {
202+
if (precomputedClient == null) {
203+
appendToLog("Client not initialized. Click 'From Server' or 'From Disk' first.");
204+
return;
205+
}
206+
207+
String flagKey = flagKeyInput.getText().toString();
208+
if (TextUtils.isEmpty(flagKey)) {
209+
appendToLog("Flag key is required");
210+
return;
211+
}
212+
213+
int selectedTypeId = flagTypeGroup.getCheckedRadioButtonId();
214+
String result;
215+
216+
try {
217+
if (selectedTypeId == R.id.type_string) {
218+
result = precomputedClient.getStringAssignment(flagKey, "(default)");
219+
appendToLog("String assignment for '" + flagKey + "': " + result);
220+
} else if (selectedTypeId == R.id.type_boolean) {
221+
boolean boolResult = precomputedClient.getBooleanAssignment(flagKey, false);
222+
appendToLog("Boolean assignment for '" + flagKey + "': " + boolResult);
223+
} else if (selectedTypeId == R.id.type_integer) {
224+
int intResult = precomputedClient.getIntegerAssignment(flagKey, 0);
225+
appendToLog("Integer assignment for '" + flagKey + "': " + intResult);
226+
} else if (selectedTypeId == R.id.type_numeric) {
227+
double numericResult = precomputedClient.getNumericAssignment(flagKey, 0.0);
228+
appendToLog("Numeric assignment for '" + flagKey + "': " + numericResult);
229+
} else if (selectedTypeId == R.id.type_json) {
230+
// JSON assignments return JsonNode - for simplicity, we show as string
231+
appendToLog("JSON assignment for '" + flagKey + "': (use getJSONAssignment() API)");
232+
} else {
233+
appendToLog("Please select a flag type");
234+
}
235+
} catch (Exception e) {
236+
appendToLog("Error getting assignment: " + e.getMessage());
237+
}
238+
}
239+
240+
private void appendToLog(String message) {
241+
assignmentLog.append(message + "\n\n");
242+
assignmentLogScrollView.post(() -> assignmentLogScrollView.fullScroll(View.FOCUS_DOWN));
243+
}
244+
245+
@Override
246+
public boolean onOptionsItemSelected(MenuItem item) {
247+
if (item.getItemId() == android.R.id.home) {
248+
finish();
249+
return true;
250+
}
251+
return super.onOptionsItemSelected(item);
252+
}
253+
254+
@Override
255+
public void onPause() {
256+
super.onPause();
257+
if (precomputedClient != null) {
258+
precomputedClient.pausePolling();
259+
}
260+
}
261+
262+
@Override
263+
public void onResume() {
264+
super.onResume();
265+
if (precomputedClient != null) {
266+
precomputedClient.resumePolling();
267+
}
268+
}
269+
270+
@Override
271+
public void onDestroy() {
272+
super.onDestroy();
273+
if (precomputedClient != null) {
274+
precomputedClient.stopPolling();
275+
}
276+
}
277+
}

0 commit comments

Comments
 (0)