536 lines
9.5 KiB
Markdown
536 lines
9.5 KiB
Markdown
# Best Practices
|
|
|
|
Guidelines for building high-quality Mosis apps that users love.
|
|
|
|
## Performance
|
|
|
|
### Minimize DOM Queries
|
|
|
|
Cache element references instead of querying repeatedly:
|
|
|
|
```lua
|
|
-- Bad: Queries on every frame
|
|
function updateScore()
|
|
document:GetElementById("score").inner_rml = tostring(score)
|
|
end
|
|
|
|
-- Good: Cache the reference
|
|
local scoreElement
|
|
|
|
function onLoad()
|
|
scoreElement = document:GetElementById("score")
|
|
end
|
|
|
|
function updateScore()
|
|
scoreElement.inner_rml = tostring(score)
|
|
end
|
|
```
|
|
|
|
### Batch DOM Updates
|
|
|
|
Group multiple changes together:
|
|
|
|
```lua
|
|
-- Bad: Multiple separate updates
|
|
elem1.style.color = "red"
|
|
elem2.style.color = "red"
|
|
elem3.style.color = "red"
|
|
|
|
-- Good: Use a class
|
|
parent:SetClass("error-state", true)
|
|
```
|
|
|
|
### Use Efficient Data Structures
|
|
|
|
```lua
|
|
-- For frequent lookups, use tables as maps
|
|
local itemLookup = {}
|
|
for i, item in ipairs(items) do
|
|
itemLookup[item.id] = item
|
|
end
|
|
|
|
-- O(1) lookup instead of O(n) search
|
|
local item = itemLookup["item-123"]
|
|
```
|
|
|
|
### Clean Up Timers
|
|
|
|
Always clear intervals when navigating away:
|
|
|
|
```lua
|
|
local updateInterval
|
|
|
|
function onScreenLoad()
|
|
updateInterval = setInterval(function()
|
|
updateData()
|
|
end, 1000)
|
|
end
|
|
|
|
function onScreenUnload()
|
|
if updateInterval then
|
|
clearInterval(updateInterval)
|
|
updateInterval = nil
|
|
end
|
|
end
|
|
```
|
|
|
|
### Lazy Load Content
|
|
|
|
Don't load everything at startup:
|
|
|
|
```lua
|
|
-- Load data when user scrolls to section
|
|
function onSectionVisible(sectionId)
|
|
if not loadedSections[sectionId] then
|
|
loadSectionData(sectionId)
|
|
loadedSections[sectionId] = true
|
|
end
|
|
end
|
|
```
|
|
|
|
## User Experience
|
|
|
|
### Provide Feedback
|
|
|
|
Show users that actions are happening:
|
|
|
|
```lua
|
|
function onSubmit()
|
|
-- Show loading state immediately
|
|
submitButton:SetClass("loading", true)
|
|
submitButton:SetAttribute("disabled", "disabled")
|
|
|
|
http.post(url, data, function(response)
|
|
submitButton:SetClass("loading", false)
|
|
submitButton:RemoveAttribute("disabled")
|
|
|
|
if response.ok then
|
|
showSuccess("Saved!")
|
|
else
|
|
showError("Failed to save")
|
|
end
|
|
end)
|
|
end
|
|
```
|
|
|
|
### Handle Errors Gracefully
|
|
|
|
Never show raw error messages to users:
|
|
|
|
```lua
|
|
http.get(url, function(response)
|
|
if response.ok then
|
|
displayData(json.decode(response.body))
|
|
else
|
|
-- User-friendly message
|
|
showMessage("Unable to load data. Please check your connection.")
|
|
|
|
-- Log details for debugging
|
|
console.error("API error:", response.status, response.body)
|
|
end
|
|
end)
|
|
```
|
|
|
|
### Make Touch Targets Large Enough
|
|
|
|
Minimum 48dp for touchable elements:
|
|
|
|
```css
|
|
.button {
|
|
min-width: 48dp;
|
|
min-height: 48dp;
|
|
padding: 12dp 24dp;
|
|
}
|
|
|
|
.list-item {
|
|
min-height: 56dp;
|
|
padding: 16dp;
|
|
}
|
|
```
|
|
|
|
### Support Undo for Destructive Actions
|
|
|
|
```lua
|
|
local deletedItem = nil
|
|
local undoTimeout = nil
|
|
|
|
function deleteItem(itemId)
|
|
deletedItem = items[itemId]
|
|
items[itemId] = nil
|
|
updateList()
|
|
|
|
showUndoSnackbar("Item deleted", function()
|
|
-- Undo callback
|
|
items[itemId] = deletedItem
|
|
deletedItem = nil
|
|
updateList()
|
|
end)
|
|
|
|
-- Clear undo after 5 seconds
|
|
undoTimeout = setTimeout(function()
|
|
deletedItem = nil
|
|
permanentlyDelete(itemId)
|
|
end, 5000)
|
|
end
|
|
```
|
|
|
|
### Remember User State
|
|
|
|
Restore position and selections when returning:
|
|
|
|
```lua
|
|
function onScreenUnload()
|
|
storage.set("list_scroll_position", scrollContainer.scroll_top)
|
|
storage.set("selected_tab", currentTab)
|
|
end
|
|
|
|
function onScreenLoad()
|
|
local scrollPos = storage.get("list_scroll_position")
|
|
if scrollPos then
|
|
scrollContainer.scroll_top = scrollPos
|
|
end
|
|
|
|
local tab = storage.get("selected_tab")
|
|
if tab then
|
|
selectTab(tab)
|
|
end
|
|
end
|
|
```
|
|
|
|
## Code Quality
|
|
|
|
### Use Local Variables
|
|
|
|
Local variables are faster and prevent global pollution:
|
|
|
|
```lua
|
|
-- Bad: Global
|
|
count = 0
|
|
|
|
-- Good: Local
|
|
local count = 0
|
|
|
|
-- Good: Module-level local
|
|
local Utils = {}
|
|
local cache = {} -- Private to module
|
|
```
|
|
|
|
### Handle Edge Cases
|
|
|
|
```lua
|
|
function divide(a, b)
|
|
if b == 0 then
|
|
console.warn("Division by zero")
|
|
return 0
|
|
end
|
|
return a / b
|
|
end
|
|
|
|
function getUsername(user)
|
|
if not user then
|
|
return "Unknown"
|
|
end
|
|
return user.name or user.email or "Unknown"
|
|
end
|
|
```
|
|
|
|
### Use Meaningful Names
|
|
|
|
```lua
|
|
-- Bad
|
|
local t = {}
|
|
local n = 0
|
|
|
|
-- Good
|
|
local userScores = {}
|
|
local attemptCount = 0
|
|
|
|
-- Bad
|
|
function p(x)
|
|
return x * 100
|
|
end
|
|
|
|
-- Good
|
|
function toPercentage(decimal)
|
|
return decimal * 100
|
|
end
|
|
```
|
|
|
|
### Keep Functions Small
|
|
|
|
Each function should do one thing:
|
|
|
|
```lua
|
|
-- Bad: Does too much
|
|
function processUser(userId)
|
|
local user = fetchUser(userId)
|
|
validateUser(user)
|
|
updateUserStats(user)
|
|
sendWelcomeEmail(user)
|
|
logActivity(user)
|
|
return formatUserResponse(user)
|
|
end
|
|
|
|
-- Good: Composed of small functions
|
|
function processNewUser(userId)
|
|
local user = fetchUser(userId)
|
|
if not isValidUser(user) then
|
|
return nil, "Invalid user"
|
|
end
|
|
initializeUserStats(user)
|
|
queueWelcomeEmail(user)
|
|
return user
|
|
end
|
|
```
|
|
|
|
### Comment Why, Not What
|
|
|
|
```lua
|
|
-- Bad: Describes what (obvious from code)
|
|
-- Increment counter by 1
|
|
counter = counter + 1
|
|
|
|
-- Good: Explains why
|
|
-- Reset retry count after successful connection
|
|
-- to prevent unnecessary backoff on next attempt
|
|
retryCount = 0
|
|
```
|
|
|
|
## Security
|
|
|
|
### Validate All Input
|
|
|
|
```lua
|
|
function searchItems(query)
|
|
-- Sanitize input
|
|
if type(query) ~= "string" then
|
|
return {}
|
|
end
|
|
|
|
query = query:sub(1, 100) -- Limit length
|
|
query = query:gsub("[^%w%s]", "") -- Remove special chars
|
|
|
|
return performSearch(query)
|
|
end
|
|
```
|
|
|
|
### Don't Trust External Data
|
|
|
|
```lua
|
|
http.get(url, function(response)
|
|
local success, data = pcall(function()
|
|
return json.decode(response.body)
|
|
end)
|
|
|
|
if not success then
|
|
console.error("Invalid JSON from API")
|
|
return
|
|
end
|
|
|
|
-- Validate structure
|
|
if type(data.items) ~= "table" then
|
|
console.error("Missing items array")
|
|
return
|
|
end
|
|
|
|
processItems(data.items)
|
|
end)
|
|
```
|
|
|
|
### Never Store Secrets in Code
|
|
|
|
```lua
|
|
-- Bad: Hardcoded API key
|
|
local API_KEY = "sk-12345abcde"
|
|
|
|
-- Good: Use environment/config
|
|
local apiKey = config.get("api_key")
|
|
```
|
|
|
|
### Sanitize Display Content
|
|
|
|
When displaying user-generated content, prevent injection:
|
|
|
|
```lua
|
|
function displayComment(text)
|
|
-- Escape HTML entities
|
|
text = text:gsub("&", "&")
|
|
text = text:gsub("<", "<")
|
|
text = text:gsub(">", ">")
|
|
|
|
commentElement.inner_rml = text
|
|
end
|
|
```
|
|
|
|
## Accessibility
|
|
|
|
### Use Semantic Elements
|
|
|
|
```xml
|
|
<!-- Bad: Divs for everything -->
|
|
<div class="button" onclick="submit()">Submit</div>
|
|
|
|
<!-- Good: Proper elements -->
|
|
<button onclick="submit()">Submit</button>
|
|
|
|
<!-- Good: Headings create hierarchy -->
|
|
<h1>Settings</h1>
|
|
<h2>Account</h2>
|
|
<h2>Notifications</h2>
|
|
```
|
|
|
|
### Provide Text Alternatives
|
|
|
|
```xml
|
|
<!-- Images should describe their purpose -->
|
|
<img src="icons/search.tga" alt="Search"/>
|
|
|
|
<!-- Icons with meaning need labels -->
|
|
<button aria-label="Close">
|
|
<img src="icons/close.tga"/>
|
|
</button>
|
|
```
|
|
|
|
### Ensure Color Contrast
|
|
|
|
Text should have at least 4.5:1 contrast ratio:
|
|
|
|
```css
|
|
/* Good contrast */
|
|
.light-text {
|
|
color: #ffffff;
|
|
background-color: #1a1a2e; /* Contrast: 12.6:1 */
|
|
}
|
|
|
|
/* Bad contrast */
|
|
.low-contrast {
|
|
color: #888888;
|
|
background-color: #666666; /* Contrast: 1.3:1 */
|
|
}
|
|
```
|
|
|
|
### Don't Rely on Color Alone
|
|
|
|
```css
|
|
/* Bad: Only color indicates error */
|
|
.error {
|
|
color: red;
|
|
}
|
|
|
|
/* Good: Icon + color + text */
|
|
.error {
|
|
color: #ff4444;
|
|
}
|
|
.error::before {
|
|
content: "⚠ ";
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Test Error States
|
|
|
|
Don't just test the happy path:
|
|
|
|
```lua
|
|
-- Test these scenarios:
|
|
-- 1. Empty data
|
|
-- 2. Network failure
|
|
-- 3. Invalid input
|
|
-- 4. Timeouts
|
|
-- 5. Missing permissions
|
|
```
|
|
|
|
### Test Navigation Flows
|
|
|
|
Ensure users can:
|
|
- Navigate forward and back
|
|
- Return to the home screen
|
|
- Handle the back button at any screen
|
|
|
|
### Test Edge Cases
|
|
|
|
- Very long text/names
|
|
- Empty lists
|
|
- Maximum values
|
|
- Rapid repeated actions
|
|
- Interrupted operations
|
|
|
|
### Use Debug Logging
|
|
|
|
```lua
|
|
local DEBUG = true
|
|
|
|
function debugLog(...)
|
|
if DEBUG then
|
|
print("[DEBUG]", ...)
|
|
end
|
|
end
|
|
|
|
-- In production build, set DEBUG = false
|
|
```
|
|
|
|
## Deployment
|
|
|
|
### Use Meaningful Version Numbers
|
|
|
|
Follow semantic versioning:
|
|
- **MAJOR**: Breaking changes
|
|
- **MINOR**: New features, backward compatible
|
|
- **PATCH**: Bug fixes
|
|
|
|
```json
|
|
{
|
|
"version": "2.1.3",
|
|
"version_code": 15
|
|
}
|
|
```
|
|
|
|
### Write Good Release Notes
|
|
|
|
```
|
|
Version 2.1.0
|
|
|
|
New Features:
|
|
- Added dark mode support
|
|
- New export to PDF feature
|
|
|
|
Improvements:
|
|
- Faster loading times
|
|
- Better error messages
|
|
|
|
Bug Fixes:
|
|
- Fixed crash when opening empty files
|
|
- Fixed date format on some devices
|
|
```
|
|
|
|
### Test Before Submitting
|
|
|
|
1. Run on the Designer
|
|
2. Test all features manually
|
|
3. Check on a real device if possible
|
|
4. Verify all assets load correctly
|
|
5. Test offline behavior
|
|
|
|
## Summary Checklist
|
|
|
|
Before submitting your app:
|
|
|
|
- [ ] All features work as expected
|
|
- [ ] Error states are handled gracefully
|
|
- [ ] Loading states shown during async operations
|
|
- [ ] Touch targets are at least 48dp
|
|
- [ ] Text is readable (contrast ratio ≥ 4.5:1)
|
|
- [ ] No console errors in normal usage
|
|
- [ ] Timers and intervals cleaned up properly
|
|
- [ ] User data persists correctly
|
|
- [ ] App works after fresh install
|
|
- [ ] Version number and code are updated
|
|
- [ ] Release notes are meaningful
|
|
|
|
## See Also
|
|
|
|
- [UI Design Guide](ui-design.md) - Design patterns
|
|
- [Lua Scripting Guide](lua-scripting.md) - Code patterns
|
|
- [Troubleshooting](../troubleshooting.md) - Common issues
|