9.5 KiB
9.5 KiB
Best Practices
Guidelines for building high-quality Mosis apps that users love.
Performance
Minimize DOM Queries
Cache element references instead of querying repeatedly:
-- 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:
-- 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
-- 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:
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:
-- 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:
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:
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:
.button {
min-width: 48dp;
min-height: 48dp;
padding: 12dp 24dp;
}
.list-item {
min-height: 56dp;
padding: 16dp;
}
Support Undo for Destructive Actions
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:
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:
-- Bad: Global
count = 0
-- Good: Local
local count = 0
-- Good: Module-level local
local Utils = {}
local cache = {} -- Private to module
Handle Edge Cases
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
-- 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:
-- 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
-- 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
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
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
-- 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:
function displayComment(text)
-- Escape HTML entities
text = text:gsub("&", "&")
text = text:gsub("<", "<")
text = text:gsub(">", ">")
commentElement.inner_rml = text
end
Accessibility
Use Semantic Elements
<!-- 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
<!-- 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:
/* 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
/* 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:
-- 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
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
{
"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
- Run on the Designer
- Test all features manually
- Check on a real device if possible
- Verify all assets load correctly
- 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 - Design patterns
- Lua Scripting Guide - Code patterns
- Troubleshooting - Common issues