中文版請見 CONTRIBUTING_zh.md
This guide is for developers who want to add features, fix bugs, or submit improvements to Gsyn Java.
Keep it readable over clever.
This project is also an educational reference. Prefer an explicit 10-line method over a clever 2-line lambda that requires careful thought to understand. Junior developers and students should be able to follow the logic without a debugger.
main → stable, tagged releases
feature/xxx → new feature branch
fix/xxx → bug fix branch
Commit message format:
type: short description (under 72 chars)
- bullet point detail
- another detail
Types: feat, fix, refactor, docs, test, chore
- Read the relevant section of ARCHITECTURE.md
- Read UI_PATTERNS.md so your code follows existing patterns
- Run
./gradlew assembleDebugto confirm the project compiles on your machine - Check if there are existing helper methods in
UiFormatters,AppRepository, orAppThemeConfigbefore writing new ones
- Create
MyNewFragment.javaextendingFragment - Create
fragment_my_new.xmllayout - All colours must use
?attr/colorXxxtheme attributes — no hardcoded hex - All strings must be in
res/values/strings.xml(EN) andres/values-zh/strings.xml(ZH) - Add navigation in
MainActivity:- Bottom nav tab → add item to
res/menu/bottom_nav.xml+ handle insetOnItemSelectedListener - Drawer extension → add item to
res/menu/drawer_nav.xml+ handle inonNavigationItemSelected
- Bottom nav tab → add item to
- Create
MyTransport.javaintransport/ - Implement
start(),stop(),send(byte[]) - On each received packet, call:
DecodeResult result = PacketDecoder.decode(rawBytes); if (result.valid) { List<SensorReading> readings = DiffEngine.process(result.cmd, result.aid, result.rawBody); repository.insertSensorDataBatch(result.aid, readings); rulesEngine.evaluate(result.aid, readings); notifyMessageListeners(new DeviceMessage(result.aid, readings)); }
- Register start/stop in
TransportManagerand expose a toggle inSettingsFragment
- Add a new string constant in
RulesEngine(e.g."send_webhook") - Add a
casebranch inRulesEngine.evaluate()with the action implementation - Add the option to the rule creation dialog in
RulesMirrorFragment - Add the string resource for the new action type in both language files
When adding any user-visible text:
res/values/strings.xml → English (mandatory)
res/values-zh/strings.xml → Chinese (mandatory)
Name format: section_description, e.g.:
dashboard_label_total_devicessettings_single_device_modemirror_rules_dialog_title
Never use the same string key for unrelated concepts. If a phrase appears in two places, it is acceptable to have two keys (they might diverge in translation).
- Java 8 — lambdas and streams are fine; records and
varare not (minSdk 24 requires desugaring) - 4-space indentation
- No wildcard imports (
import java.util.*is forbidden) - Null safety — always null-check
bindingbefore view access in Fragment methods that can be called async - Background thread guard — any callback from
TransportManageris on a background thread; alwaysrunOnUiThread()before touching views
Unit tests live in app/src/test/. The protocol layer is fully testable without Android:
// Example: test packet decode
@Test
public void testDecodeValidPacket() {
byte[] packet = buildTestPacket(CMD_DATA_FULL, 42, ...);
DecodeResult result = PacketDecoder.decode(packet);
assertTrue(result.valid);
assertEquals(42, result.aid);
}Instrumented tests (requiring a device/emulator) go in app/src/androidTest/.
Run all unit tests:
./gradlew test- Update
versionCodeandversionNameinapp/build.gradle git add -A && git commit -m "chore: bump version to x.y.z"git tag -a vx.y.z -m "vx.y.z: Release notes here"git push origin main --tags- GitHub Actions builds the signed APK automatically (see
.github/workflows/)
| Mistake | Correct Approach |
|---|---|
binding.tvFoo.setText(R.string.bar) |
binding.tvFoo.setText(getString(R.string.bar)) — R.string is an int, not a String |
Hardcoded "5 分钟前" in Java |
DateUtils.getRelativeTimeSpanString(...) |
android:textColor="#000000" in XML |
android:textColor="?attr/colorOnSurface" |
Querying DB in onBindViewHolder |
Query in Fragment.refresh(), pass data via Snapshot |
commit() for MapFragment |
Must use commitNow() — see ARCHITECTURE.md §Google Maps |
getActivity().runOnUiThread(...) without null check |
if (getActivity() != null) getActivity().runOnUiThread(...) |