claude-code-starter 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1064 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -163,6 +163,62 @@ function detectFrameworks(packageJson, files, rootDir) {
|
|
|
163
163
|
if (cargo.includes("axum")) add("axum");
|
|
164
164
|
if (cargo.includes("rocket")) add("rocket");
|
|
165
165
|
}
|
|
166
|
+
const packageSwiftPath = path.join(rootDir, "Package.swift");
|
|
167
|
+
const xcodeprojExists = files.some((f) => f.endsWith(".xcodeproj")) || files.some((f) => f.endsWith(".xcworkspace"));
|
|
168
|
+
if (fs.existsSync(packageSwiftPath)) {
|
|
169
|
+
const packageSwift = fs.readFileSync(packageSwiftPath, "utf-8").toLowerCase();
|
|
170
|
+
if (packageSwift.includes("vapor")) add("vapor");
|
|
171
|
+
}
|
|
172
|
+
if (xcodeprojExists || files.some((f) => f.endsWith(".swift"))) {
|
|
173
|
+
const srcFiles = listSourceFilesShallow(rootDir, [".swift"]);
|
|
174
|
+
const hasSwiftUI = srcFiles.some(
|
|
175
|
+
(f) => f.includes("ContentView") || f.includes("App.swift") || f.includes("@main struct")
|
|
176
|
+
);
|
|
177
|
+
const hasUIKit = srcFiles.some(
|
|
178
|
+
(f) => f.includes("ViewController") || f.includes("AppDelegate") || f.includes("SceneDelegate")
|
|
179
|
+
);
|
|
180
|
+
if (hasSwiftUI) add("swiftui");
|
|
181
|
+
if (hasUIKit) add("uikit");
|
|
182
|
+
for (const file of srcFiles.slice(0, 10)) {
|
|
183
|
+
try {
|
|
184
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
185
|
+
if (content.includes("import SwiftData") || content.includes("@Model")) add("swiftdata");
|
|
186
|
+
if (content.includes("import Combine") || content.includes("PassthroughSubject"))
|
|
187
|
+
add("combine");
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const gradlePath = path.join(rootDir, "build.gradle");
|
|
193
|
+
const gradleKtsPath = path.join(rootDir, "build.gradle.kts");
|
|
194
|
+
const appGradlePath = path.join(rootDir, "app", "build.gradle");
|
|
195
|
+
const appGradleKtsPath = path.join(rootDir, "app", "build.gradle.kts");
|
|
196
|
+
let gradleContent = "";
|
|
197
|
+
for (const gPath of [gradlePath, gradleKtsPath, appGradlePath, appGradleKtsPath]) {
|
|
198
|
+
if (fs.existsSync(gPath)) {
|
|
199
|
+
gradleContent += fs.readFileSync(gPath, "utf-8").toLowerCase();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (gradleContent) {
|
|
203
|
+
if (gradleContent.includes("com.android") || gradleContent.includes("android {")) {
|
|
204
|
+
if (gradleContent.includes("compose") || gradleContent.includes("androidx.compose") || gradleContent.includes("composeoptions")) {
|
|
205
|
+
add("jetpack-compose");
|
|
206
|
+
} else {
|
|
207
|
+
add("android-views");
|
|
208
|
+
}
|
|
209
|
+
if (gradleContent.includes("room") || gradleContent.includes("androidx.room")) {
|
|
210
|
+
add("room");
|
|
211
|
+
}
|
|
212
|
+
if (gradleContent.includes("hilt") || gradleContent.includes("dagger.hilt")) {
|
|
213
|
+
add("hilt");
|
|
214
|
+
}
|
|
215
|
+
if (gradleContent.includes("ktor")) {
|
|
216
|
+
add("ktor-android");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (gradleContent.includes("spring")) add("spring");
|
|
220
|
+
if (gradleContent.includes("quarkus")) add("quarkus");
|
|
221
|
+
}
|
|
166
222
|
return frameworks;
|
|
167
223
|
}
|
|
168
224
|
const allDeps = {
|
|
@@ -356,6 +412,27 @@ function hasDevDep(packageJson, dep) {
|
|
|
356
412
|
const deps = packageJson.dependencies || {};
|
|
357
413
|
return dep in devDeps || dep in deps;
|
|
358
414
|
}
|
|
415
|
+
function listSourceFilesShallow(rootDir, extensions) {
|
|
416
|
+
const files = [];
|
|
417
|
+
const ignoreDirs = ["node_modules", ".git", "build", "dist", "Pods", ".build", "DerivedData"];
|
|
418
|
+
function scan(dir, depth) {
|
|
419
|
+
if (depth > 2) return;
|
|
420
|
+
try {
|
|
421
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
422
|
+
if (ignoreDirs.includes(entry.name)) continue;
|
|
423
|
+
const fullPath = path.join(dir, entry.name);
|
|
424
|
+
if (entry.isDirectory()) {
|
|
425
|
+
scan(fullPath, depth + 1);
|
|
426
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
427
|
+
files.push(fullPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
scan(rootDir, 0);
|
|
434
|
+
return files;
|
|
435
|
+
}
|
|
359
436
|
function countSourceFiles(rootDir, _languages) {
|
|
360
437
|
const extensions = [
|
|
361
438
|
// JavaScript/TypeScript
|
|
@@ -796,6 +873,21 @@ function getSkillsForStack(stack) {
|
|
|
796
873
|
if (stack.frameworks.includes("prisma") || stack.frameworks.includes("drizzle")) {
|
|
797
874
|
skills.push({ name: "database-patterns", description: "Database and ORM patterns" });
|
|
798
875
|
}
|
|
876
|
+
if (stack.frameworks.includes("swiftui")) {
|
|
877
|
+
skills.push({ name: "swiftui-patterns", description: "SwiftUI declarative UI patterns" });
|
|
878
|
+
}
|
|
879
|
+
if (stack.frameworks.includes("uikit")) {
|
|
880
|
+
skills.push({ name: "uikit-patterns", description: "UIKit view controller patterns" });
|
|
881
|
+
}
|
|
882
|
+
if (stack.frameworks.includes("vapor")) {
|
|
883
|
+
skills.push({ name: "vapor-patterns", description: "Vapor server-side Swift patterns" });
|
|
884
|
+
}
|
|
885
|
+
if (stack.frameworks.includes("jetpack-compose")) {
|
|
886
|
+
skills.push({ name: "compose-patterns", description: "Jetpack Compose UI patterns" });
|
|
887
|
+
}
|
|
888
|
+
if (stack.frameworks.includes("android-views")) {
|
|
889
|
+
skills.push({ name: "android-views-patterns", description: "Android XML views patterns" });
|
|
890
|
+
}
|
|
799
891
|
return skills;
|
|
800
892
|
}
|
|
801
893
|
function generateSkills(stack) {
|
|
@@ -820,6 +912,21 @@ function generateSkills(stack) {
|
|
|
820
912
|
if (stack.frameworks.includes("nestjs")) {
|
|
821
913
|
artifacts.push(generateNestJSSkill());
|
|
822
914
|
}
|
|
915
|
+
if (stack.frameworks.includes("swiftui")) {
|
|
916
|
+
artifacts.push(generateSwiftUISkill());
|
|
917
|
+
}
|
|
918
|
+
if (stack.frameworks.includes("uikit")) {
|
|
919
|
+
artifacts.push(generateUIKitSkill());
|
|
920
|
+
}
|
|
921
|
+
if (stack.frameworks.includes("vapor")) {
|
|
922
|
+
artifacts.push(generateVaporSkill());
|
|
923
|
+
}
|
|
924
|
+
if (stack.frameworks.includes("jetpack-compose")) {
|
|
925
|
+
artifacts.push(generateJetpackComposeSkill());
|
|
926
|
+
}
|
|
927
|
+
if (stack.frameworks.includes("android-views")) {
|
|
928
|
+
artifacts.push(generateAndroidViewsSkill());
|
|
929
|
+
}
|
|
823
930
|
return artifacts;
|
|
824
931
|
}
|
|
825
932
|
function generatePatternDiscoverySkill() {
|
|
@@ -1659,6 +1766,934 @@ describe('UsersService', () => {
|
|
|
1659
1766
|
isNew: true
|
|
1660
1767
|
};
|
|
1661
1768
|
}
|
|
1769
|
+
function generateSwiftUISkill() {
|
|
1770
|
+
return {
|
|
1771
|
+
type: "skill",
|
|
1772
|
+
path: ".claude/skills/swiftui-patterns.md",
|
|
1773
|
+
content: `---
|
|
1774
|
+
name: swiftui-patterns
|
|
1775
|
+
description: SwiftUI declarative UI patterns and best practices
|
|
1776
|
+
globs:
|
|
1777
|
+
- "**/*.swift"
|
|
1778
|
+
---
|
|
1779
|
+
|
|
1780
|
+
# SwiftUI Patterns
|
|
1781
|
+
|
|
1782
|
+
## View Structure
|
|
1783
|
+
|
|
1784
|
+
\`\`\`swift
|
|
1785
|
+
import SwiftUI
|
|
1786
|
+
|
|
1787
|
+
struct ContentView: View {
|
|
1788
|
+
@State private var count = 0
|
|
1789
|
+
@StateObject private var viewModel = ContentViewModel()
|
|
1790
|
+
|
|
1791
|
+
var body: some View {
|
|
1792
|
+
VStack(spacing: 16) {
|
|
1793
|
+
Text("Count: \\(count)")
|
|
1794
|
+
.font(.title)
|
|
1795
|
+
|
|
1796
|
+
Button("Increment") {
|
|
1797
|
+
count += 1
|
|
1798
|
+
}
|
|
1799
|
+
.buttonStyle(.borderedProminent)
|
|
1800
|
+
}
|
|
1801
|
+
.padding()
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
#Preview {
|
|
1806
|
+
ContentView()
|
|
1807
|
+
}
|
|
1808
|
+
\`\`\`
|
|
1809
|
+
|
|
1810
|
+
## Property Wrappers
|
|
1811
|
+
|
|
1812
|
+
| Wrapper | Use Case |
|
|
1813
|
+
|---------|----------|
|
|
1814
|
+
| \`@State\` | Simple value types owned by view |
|
|
1815
|
+
| \`@Binding\` | Two-way connection to parent's state |
|
|
1816
|
+
| \`@StateObject\` | Reference type owned by view (create once) |
|
|
1817
|
+
| \`@ObservedObject\` | Reference type passed from parent |
|
|
1818
|
+
| \`@EnvironmentObject\` | Shared data through view hierarchy |
|
|
1819
|
+
| \`@Environment\` | System environment values |
|
|
1820
|
+
|
|
1821
|
+
## MVVM Pattern
|
|
1822
|
+
|
|
1823
|
+
\`\`\`swift
|
|
1824
|
+
// ViewModel
|
|
1825
|
+
@MainActor
|
|
1826
|
+
class UserViewModel: ObservableObject {
|
|
1827
|
+
@Published var users: [User] = []
|
|
1828
|
+
@Published var isLoading = false
|
|
1829
|
+
@Published var error: Error?
|
|
1830
|
+
|
|
1831
|
+
private let service: UserService
|
|
1832
|
+
|
|
1833
|
+
init(service: UserService = .shared) {
|
|
1834
|
+
self.service = service
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
func fetchUsers() async {
|
|
1838
|
+
isLoading = true
|
|
1839
|
+
defer { isLoading = false }
|
|
1840
|
+
|
|
1841
|
+
do {
|
|
1842
|
+
users = try await service.getUsers()
|
|
1843
|
+
} catch {
|
|
1844
|
+
self.error = error
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// View
|
|
1850
|
+
struct UsersView: View {
|
|
1851
|
+
@StateObject private var viewModel = UserViewModel()
|
|
1852
|
+
|
|
1853
|
+
var body: some View {
|
|
1854
|
+
List(viewModel.users) { user in
|
|
1855
|
+
UserRow(user: user)
|
|
1856
|
+
}
|
|
1857
|
+
.overlay {
|
|
1858
|
+
if viewModel.isLoading {
|
|
1859
|
+
ProgressView()
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
.task {
|
|
1863
|
+
await viewModel.fetchUsers()
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
\`\`\`
|
|
1868
|
+
|
|
1869
|
+
## Navigation (iOS 16+)
|
|
1870
|
+
|
|
1871
|
+
\`\`\`swift
|
|
1872
|
+
struct AppNavigation: View {
|
|
1873
|
+
@State private var path = NavigationPath()
|
|
1874
|
+
|
|
1875
|
+
var body: some View {
|
|
1876
|
+
NavigationStack(path: $path) {
|
|
1877
|
+
HomeView()
|
|
1878
|
+
.navigationDestination(for: User.self) { user in
|
|
1879
|
+
UserDetailView(user: user)
|
|
1880
|
+
}
|
|
1881
|
+
.navigationDestination(for: Settings.self) { settings in
|
|
1882
|
+
SettingsView(settings: settings)
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
\`\`\`
|
|
1888
|
+
|
|
1889
|
+
## Async/Await Patterns
|
|
1890
|
+
|
|
1891
|
+
\`\`\`swift
|
|
1892
|
+
// Task modifier for view lifecycle
|
|
1893
|
+
.task {
|
|
1894
|
+
await loadData()
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Task with cancellation
|
|
1898
|
+
.task(id: searchText) {
|
|
1899
|
+
try? await Task.sleep(for: .milliseconds(300))
|
|
1900
|
+
await search(searchText)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Refreshable
|
|
1904
|
+
.refreshable {
|
|
1905
|
+
await viewModel.refresh()
|
|
1906
|
+
}
|
|
1907
|
+
\`\`\`
|
|
1908
|
+
|
|
1909
|
+
## Best Practices
|
|
1910
|
+
|
|
1911
|
+
1. **Keep views small** - Extract subviews for reusability
|
|
1912
|
+
2. **Use \`@MainActor\`** - For ViewModels updating UI
|
|
1913
|
+
3. **Prefer value types** - Structs over classes when possible
|
|
1914
|
+
4. **Use \`.task\`** - Instead of \`.onAppear\` for async work
|
|
1915
|
+
5. **Preview extensively** - Use #Preview for rapid iteration
|
|
1916
|
+
`,
|
|
1917
|
+
isNew: true
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function generateUIKitSkill() {
|
|
1921
|
+
return {
|
|
1922
|
+
type: "skill",
|
|
1923
|
+
path: ".claude/skills/uikit-patterns.md",
|
|
1924
|
+
content: `---
|
|
1925
|
+
name: uikit-patterns
|
|
1926
|
+
description: UIKit view controller patterns and best practices
|
|
1927
|
+
globs:
|
|
1928
|
+
- "**/*.swift"
|
|
1929
|
+
---
|
|
1930
|
+
|
|
1931
|
+
# UIKit Patterns
|
|
1932
|
+
|
|
1933
|
+
## View Controller Structure
|
|
1934
|
+
|
|
1935
|
+
\`\`\`swift
|
|
1936
|
+
import UIKit
|
|
1937
|
+
|
|
1938
|
+
class UserViewController: UIViewController {
|
|
1939
|
+
// MARK: - Properties
|
|
1940
|
+
private let viewModel: UserViewModel
|
|
1941
|
+
private var cancellables = Set<AnyCancellable>()
|
|
1942
|
+
|
|
1943
|
+
// MARK: - UI Components
|
|
1944
|
+
private lazy var tableView: UITableView = {
|
|
1945
|
+
let table = UITableView()
|
|
1946
|
+
table.translatesAutoresizingMaskIntoConstraints = false
|
|
1947
|
+
table.delegate = self
|
|
1948
|
+
table.dataSource = self
|
|
1949
|
+
table.register(UserCell.self, forCellReuseIdentifier: UserCell.identifier)
|
|
1950
|
+
return table
|
|
1951
|
+
}()
|
|
1952
|
+
|
|
1953
|
+
// MARK: - Lifecycle
|
|
1954
|
+
init(viewModel: UserViewModel) {
|
|
1955
|
+
self.viewModel = viewModel
|
|
1956
|
+
super.init(nibName: nil, bundle: nil)
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
required init?(coder: NSCoder) {
|
|
1960
|
+
fatalError("init(coder:) has not been implemented")
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
override func viewDidLoad() {
|
|
1964
|
+
super.viewDidLoad()
|
|
1965
|
+
setupUI()
|
|
1966
|
+
setupBindings()
|
|
1967
|
+
viewModel.fetchUsers()
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// MARK: - Setup
|
|
1971
|
+
private func setupUI() {
|
|
1972
|
+
view.backgroundColor = .systemBackground
|
|
1973
|
+
view.addSubview(tableView)
|
|
1974
|
+
|
|
1975
|
+
NSLayoutConstraint.activate([
|
|
1976
|
+
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
1977
|
+
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
1978
|
+
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
1979
|
+
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
1980
|
+
])
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
private func setupBindings() {
|
|
1984
|
+
viewModel.$users
|
|
1985
|
+
.receive(on: DispatchQueue.main)
|
|
1986
|
+
.sink { [weak self] _ in
|
|
1987
|
+
self?.tableView.reloadData()
|
|
1988
|
+
}
|
|
1989
|
+
.store(in: &cancellables)
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
\`\`\`
|
|
1993
|
+
|
|
1994
|
+
## Coordinator Pattern
|
|
1995
|
+
|
|
1996
|
+
\`\`\`swift
|
|
1997
|
+
protocol Coordinator: AnyObject {
|
|
1998
|
+
var childCoordinators: [Coordinator] { get set }
|
|
1999
|
+
var navigationController: UINavigationController { get set }
|
|
2000
|
+
func start()
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
class AppCoordinator: Coordinator {
|
|
2004
|
+
var childCoordinators: [Coordinator] = []
|
|
2005
|
+
var navigationController: UINavigationController
|
|
2006
|
+
|
|
2007
|
+
init(navigationController: UINavigationController) {
|
|
2008
|
+
self.navigationController = navigationController
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
func start() {
|
|
2012
|
+
let vc = HomeViewController()
|
|
2013
|
+
vc.coordinator = self
|
|
2014
|
+
navigationController.pushViewController(vc, animated: false)
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
func showUserDetail(_ user: User) {
|
|
2018
|
+
let vc = UserDetailViewController(user: user)
|
|
2019
|
+
navigationController.pushViewController(vc, animated: true)
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
\`\`\`
|
|
2023
|
+
|
|
2024
|
+
## Table View Cell
|
|
2025
|
+
|
|
2026
|
+
\`\`\`swift
|
|
2027
|
+
class UserCell: UITableViewCell {
|
|
2028
|
+
static let identifier = "UserCell"
|
|
2029
|
+
|
|
2030
|
+
private let nameLabel: UILabel = {
|
|
2031
|
+
let label = UILabel()
|
|
2032
|
+
label.font = .preferredFont(forTextStyle: .headline)
|
|
2033
|
+
label.translatesAutoresizingMaskIntoConstraints = false
|
|
2034
|
+
return label
|
|
2035
|
+
}()
|
|
2036
|
+
|
|
2037
|
+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
2038
|
+
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
2039
|
+
setupUI()
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
required init?(coder: NSCoder) {
|
|
2043
|
+
fatalError("init(coder:) has not been implemented")
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private func setupUI() {
|
|
2047
|
+
contentView.addSubview(nameLabel)
|
|
2048
|
+
NSLayoutConstraint.activate([
|
|
2049
|
+
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
|
2050
|
+
nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
|
|
2051
|
+
])
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
func configure(with user: User) {
|
|
2055
|
+
nameLabel.text = user.name
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
\`\`\`
|
|
2059
|
+
|
|
2060
|
+
## Best Practices
|
|
2061
|
+
|
|
2062
|
+
1. **Use Auto Layout** - Programmatic constraints over Storyboards
|
|
2063
|
+
2. **MARK comments** - Organize code sections
|
|
2064
|
+
3. **Coordinator pattern** - For navigation logic
|
|
2065
|
+
4. **Dependency injection** - Pass dependencies via init
|
|
2066
|
+
5. **Combine for bindings** - Reactive updates from ViewModel
|
|
2067
|
+
`,
|
|
2068
|
+
isNew: true
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function generateVaporSkill() {
|
|
2072
|
+
return {
|
|
2073
|
+
type: "skill",
|
|
2074
|
+
path: ".claude/skills/vapor-patterns.md",
|
|
2075
|
+
content: `---
|
|
2076
|
+
name: vapor-patterns
|
|
2077
|
+
description: Vapor server-side Swift patterns
|
|
2078
|
+
globs:
|
|
2079
|
+
- "**/*.swift"
|
|
2080
|
+
- "Package.swift"
|
|
2081
|
+
---
|
|
2082
|
+
|
|
2083
|
+
# Vapor Patterns
|
|
2084
|
+
|
|
2085
|
+
## Route Structure
|
|
2086
|
+
|
|
2087
|
+
\`\`\`swift
|
|
2088
|
+
import Vapor
|
|
2089
|
+
|
|
2090
|
+
func routes(_ app: Application) throws {
|
|
2091
|
+
// Basic routes
|
|
2092
|
+
app.get { req in
|
|
2093
|
+
"Hello, world!"
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Route groups
|
|
2097
|
+
let api = app.grouped("api", "v1")
|
|
2098
|
+
|
|
2099
|
+
// Controller registration
|
|
2100
|
+
try api.register(collection: UserController())
|
|
2101
|
+
}
|
|
2102
|
+
\`\`\`
|
|
2103
|
+
|
|
2104
|
+
## Controller Pattern
|
|
2105
|
+
|
|
2106
|
+
\`\`\`swift
|
|
2107
|
+
import Vapor
|
|
2108
|
+
|
|
2109
|
+
struct UserController: RouteCollection {
|
|
2110
|
+
func boot(routes: RoutesBuilder) throws {
|
|
2111
|
+
let users = routes.grouped("users")
|
|
2112
|
+
|
|
2113
|
+
users.get(use: index)
|
|
2114
|
+
users.post(use: create)
|
|
2115
|
+
users.group(":userID") { user in
|
|
2116
|
+
user.get(use: show)
|
|
2117
|
+
user.put(use: update)
|
|
2118
|
+
user.delete(use: delete)
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// GET /users
|
|
2123
|
+
func index(req: Request) async throws -> [UserDTO] {
|
|
2124
|
+
try await User.query(on: req.db).all().map { $0.toDTO() }
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// POST /users
|
|
2128
|
+
func create(req: Request) async throws -> UserDTO {
|
|
2129
|
+
let input = try req.content.decode(CreateUserInput.self)
|
|
2130
|
+
let user = User(name: input.name, email: input.email)
|
|
2131
|
+
try await user.save(on: req.db)
|
|
2132
|
+
return user.toDTO()
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// GET /users/:userID
|
|
2136
|
+
func show(req: Request) async throws -> UserDTO {
|
|
2137
|
+
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
|
|
2138
|
+
throw Abort(.notFound)
|
|
2139
|
+
}
|
|
2140
|
+
return user.toDTO()
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
\`\`\`
|
|
2144
|
+
|
|
2145
|
+
## Fluent Models
|
|
2146
|
+
|
|
2147
|
+
\`\`\`swift
|
|
2148
|
+
import Fluent
|
|
2149
|
+
import Vapor
|
|
2150
|
+
|
|
2151
|
+
final class User: Model, Content {
|
|
2152
|
+
static let schema = "users"
|
|
2153
|
+
|
|
2154
|
+
@ID(key: .id)
|
|
2155
|
+
var id: UUID?
|
|
2156
|
+
|
|
2157
|
+
@Field(key: "name")
|
|
2158
|
+
var name: String
|
|
2159
|
+
|
|
2160
|
+
@Field(key: "email")
|
|
2161
|
+
var email: String
|
|
2162
|
+
|
|
2163
|
+
@Timestamp(key: "created_at", on: .create)
|
|
2164
|
+
var createdAt: Date?
|
|
2165
|
+
|
|
2166
|
+
@Children(for: \\.$user)
|
|
2167
|
+
var posts: [Post]
|
|
2168
|
+
|
|
2169
|
+
init() {}
|
|
2170
|
+
|
|
2171
|
+
init(id: UUID? = nil, name: String, email: String) {
|
|
2172
|
+
self.id = id
|
|
2173
|
+
self.name = name
|
|
2174
|
+
self.email = email
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Migration
|
|
2179
|
+
struct CreateUser: AsyncMigration {
|
|
2180
|
+
func prepare(on database: Database) async throws {
|
|
2181
|
+
try await database.schema("users")
|
|
2182
|
+
.id()
|
|
2183
|
+
.field("name", .string, .required)
|
|
2184
|
+
.field("email", .string, .required)
|
|
2185
|
+
.field("created_at", .datetime)
|
|
2186
|
+
.unique(on: "email")
|
|
2187
|
+
.create()
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
func revert(on database: Database) async throws {
|
|
2191
|
+
try await database.schema("users").delete()
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
\`\`\`
|
|
2195
|
+
|
|
2196
|
+
## DTOs and Validation
|
|
2197
|
+
|
|
2198
|
+
\`\`\`swift
|
|
2199
|
+
struct CreateUserInput: Content, Validatable {
|
|
2200
|
+
var name: String
|
|
2201
|
+
var email: String
|
|
2202
|
+
|
|
2203
|
+
static func validations(_ validations: inout Validations) {
|
|
2204
|
+
validations.add("name", as: String.self, is: !.empty)
|
|
2205
|
+
validations.add("email", as: String.self, is: .email)
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
struct UserDTO: Content {
|
|
2210
|
+
var id: UUID?
|
|
2211
|
+
var name: String
|
|
2212
|
+
var email: String
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
extension User {
|
|
2216
|
+
func toDTO() -> UserDTO {
|
|
2217
|
+
UserDTO(id: id, name: name, email: email)
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
\`\`\`
|
|
2221
|
+
|
|
2222
|
+
## Middleware
|
|
2223
|
+
|
|
2224
|
+
\`\`\`swift
|
|
2225
|
+
struct AuthMiddleware: AsyncMiddleware {
|
|
2226
|
+
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
|
2227
|
+
guard let token = request.headers.bearerAuthorization?.token else {
|
|
2228
|
+
throw Abort(.unauthorized)
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Validate token
|
|
2232
|
+
let user = try await validateToken(token, on: request)
|
|
2233
|
+
request.auth.login(user)
|
|
2234
|
+
|
|
2235
|
+
return try await next.respond(to: request)
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
\`\`\`
|
|
2239
|
+
|
|
2240
|
+
## Testing
|
|
2241
|
+
|
|
2242
|
+
\`\`\`swift
|
|
2243
|
+
@testable import App
|
|
2244
|
+
import XCTVapor
|
|
2245
|
+
|
|
2246
|
+
final class UserTests: XCTestCase {
|
|
2247
|
+
var app: Application!
|
|
2248
|
+
|
|
2249
|
+
override func setUp() async throws {
|
|
2250
|
+
app = Application(.testing)
|
|
2251
|
+
try configure(app)
|
|
2252
|
+
try await app.autoMigrate()
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
override func tearDown() async throws {
|
|
2256
|
+
try await app.autoRevert()
|
|
2257
|
+
app.shutdown()
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
func testCreateUser() async throws {
|
|
2261
|
+
try app.test(.POST, "api/v1/users", beforeRequest: { req in
|
|
2262
|
+
try req.content.encode(CreateUserInput(name: "Test", email: "test@example.com"))
|
|
2263
|
+
}, afterResponse: { res in
|
|
2264
|
+
XCTAssertEqual(res.status, .ok)
|
|
2265
|
+
let user = try res.content.decode(UserDTO.self)
|
|
2266
|
+
XCTAssertEqual(user.name, "Test")
|
|
2267
|
+
})
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
\`\`\`
|
|
2271
|
+
`,
|
|
2272
|
+
isNew: true
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function generateJetpackComposeSkill() {
|
|
2276
|
+
return {
|
|
2277
|
+
type: "skill",
|
|
2278
|
+
path: ".claude/skills/compose-patterns.md",
|
|
2279
|
+
content: `---
|
|
2280
|
+
name: compose-patterns
|
|
2281
|
+
description: Jetpack Compose UI patterns and best practices
|
|
2282
|
+
globs:
|
|
2283
|
+
- "**/*.kt"
|
|
2284
|
+
---
|
|
2285
|
+
|
|
2286
|
+
# Jetpack Compose Patterns
|
|
2287
|
+
|
|
2288
|
+
## Composable Structure
|
|
2289
|
+
|
|
2290
|
+
\`\`\`kotlin
|
|
2291
|
+
@Composable
|
|
2292
|
+
fun UserScreen(
|
|
2293
|
+
viewModel: UserViewModel = hiltViewModel()
|
|
2294
|
+
) {
|
|
2295
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
2296
|
+
|
|
2297
|
+
UserScreenContent(
|
|
2298
|
+
uiState = uiState,
|
|
2299
|
+
onRefresh = viewModel::refresh,
|
|
2300
|
+
onUserClick = viewModel::selectUser
|
|
2301
|
+
)
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
@Composable
|
|
2305
|
+
private fun UserScreenContent(
|
|
2306
|
+
uiState: UserUiState,
|
|
2307
|
+
onRefresh: () -> Unit,
|
|
2308
|
+
onUserClick: (User) -> Unit
|
|
2309
|
+
) {
|
|
2310
|
+
Scaffold(
|
|
2311
|
+
topBar = {
|
|
2312
|
+
TopAppBar(title = { Text("Users") })
|
|
2313
|
+
}
|
|
2314
|
+
) { padding ->
|
|
2315
|
+
when (uiState) {
|
|
2316
|
+
is UserUiState.Loading -> LoadingIndicator()
|
|
2317
|
+
is UserUiState.Success -> UserList(
|
|
2318
|
+
users = uiState.users,
|
|
2319
|
+
onUserClick = onUserClick,
|
|
2320
|
+
modifier = Modifier.padding(padding)
|
|
2321
|
+
)
|
|
2322
|
+
is UserUiState.Error -> ErrorMessage(uiState.message)
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
\`\`\`
|
|
2327
|
+
|
|
2328
|
+
## State Management
|
|
2329
|
+
|
|
2330
|
+
\`\`\`kotlin
|
|
2331
|
+
// UI State
|
|
2332
|
+
sealed interface UserUiState {
|
|
2333
|
+
object Loading : UserUiState
|
|
2334
|
+
data class Success(val users: List<User>) : UserUiState
|
|
2335
|
+
data class Error(val message: String) : UserUiState
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// ViewModel
|
|
2339
|
+
@HiltViewModel
|
|
2340
|
+
class UserViewModel @Inject constructor(
|
|
2341
|
+
private val repository: UserRepository
|
|
2342
|
+
) : ViewModel() {
|
|
2343
|
+
|
|
2344
|
+
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
|
|
2345
|
+
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
|
|
2346
|
+
|
|
2347
|
+
init {
|
|
2348
|
+
loadUsers()
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
fun refresh() {
|
|
2352
|
+
loadUsers()
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
private fun loadUsers() {
|
|
2356
|
+
viewModelScope.launch {
|
|
2357
|
+
_uiState.value = UserUiState.Loading
|
|
2358
|
+
repository.getUsers()
|
|
2359
|
+
.onSuccess { users ->
|
|
2360
|
+
_uiState.value = UserUiState.Success(users)
|
|
2361
|
+
}
|
|
2362
|
+
.onFailure { error ->
|
|
2363
|
+
_uiState.value = UserUiState.Error(error.message ?: "Unknown error")
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
\`\`\`
|
|
2369
|
+
|
|
2370
|
+
## Reusable Components
|
|
2371
|
+
|
|
2372
|
+
\`\`\`kotlin
|
|
2373
|
+
@Composable
|
|
2374
|
+
fun UserCard(
|
|
2375
|
+
user: User,
|
|
2376
|
+
onClick: () -> Unit,
|
|
2377
|
+
modifier: Modifier = Modifier
|
|
2378
|
+
) {
|
|
2379
|
+
Card(
|
|
2380
|
+
onClick = onClick,
|
|
2381
|
+
modifier = modifier.fillMaxWidth()
|
|
2382
|
+
) {
|
|
2383
|
+
Row(
|
|
2384
|
+
modifier = Modifier.padding(16.dp),
|
|
2385
|
+
verticalAlignment = Alignment.CenterVertically
|
|
2386
|
+
) {
|
|
2387
|
+
AsyncImage(
|
|
2388
|
+
model = user.avatarUrl,
|
|
2389
|
+
contentDescription = null,
|
|
2390
|
+
modifier = Modifier
|
|
2391
|
+
.size(48.dp)
|
|
2392
|
+
.clip(CircleShape)
|
|
2393
|
+
)
|
|
2394
|
+
Spacer(modifier = Modifier.width(16.dp))
|
|
2395
|
+
Column {
|
|
2396
|
+
Text(
|
|
2397
|
+
text = user.name,
|
|
2398
|
+
style = MaterialTheme.typography.titleMedium
|
|
2399
|
+
)
|
|
2400
|
+
Text(
|
|
2401
|
+
text = user.email,
|
|
2402
|
+
style = MaterialTheme.typography.bodySmall
|
|
2403
|
+
)
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
\`\`\`
|
|
2409
|
+
|
|
2410
|
+
## Navigation
|
|
2411
|
+
|
|
2412
|
+
\`\`\`kotlin
|
|
2413
|
+
@Composable
|
|
2414
|
+
fun AppNavigation() {
|
|
2415
|
+
val navController = rememberNavController()
|
|
2416
|
+
|
|
2417
|
+
NavHost(
|
|
2418
|
+
navController = navController,
|
|
2419
|
+
startDestination = "home"
|
|
2420
|
+
) {
|
|
2421
|
+
composable("home") {
|
|
2422
|
+
HomeScreen(
|
|
2423
|
+
onUserClick = { userId ->
|
|
2424
|
+
navController.navigate("user/$userId")
|
|
2425
|
+
}
|
|
2426
|
+
)
|
|
2427
|
+
}
|
|
2428
|
+
composable(
|
|
2429
|
+
route = "user/{userId}",
|
|
2430
|
+
arguments = listOf(navArgument("userId") { type = NavType.StringType })
|
|
2431
|
+
) { backStackEntry ->
|
|
2432
|
+
val userId = backStackEntry.arguments?.getString("userId")
|
|
2433
|
+
UserDetailScreen(userId = userId)
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
\`\`\`
|
|
2438
|
+
|
|
2439
|
+
## Theming
|
|
2440
|
+
|
|
2441
|
+
\`\`\`kotlin
|
|
2442
|
+
@Composable
|
|
2443
|
+
fun AppTheme(
|
|
2444
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
2445
|
+
content: @Composable () -> Unit
|
|
2446
|
+
) {
|
|
2447
|
+
val colorScheme = if (darkTheme) {
|
|
2448
|
+
darkColorScheme(
|
|
2449
|
+
primary = Purple80,
|
|
2450
|
+
secondary = PurpleGrey80
|
|
2451
|
+
)
|
|
2452
|
+
} else {
|
|
2453
|
+
lightColorScheme(
|
|
2454
|
+
primary = Purple40,
|
|
2455
|
+
secondary = PurpleGrey40
|
|
2456
|
+
)
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
MaterialTheme(
|
|
2460
|
+
colorScheme = colorScheme,
|
|
2461
|
+
typography = Typography,
|
|
2462
|
+
content = content
|
|
2463
|
+
)
|
|
2464
|
+
}
|
|
2465
|
+
\`\`\`
|
|
2466
|
+
|
|
2467
|
+
## Best Practices
|
|
2468
|
+
|
|
2469
|
+
1. **Stateless composables** - Pass state down, events up
|
|
2470
|
+
2. **Remember wisely** - Use \`remember\` for expensive calculations
|
|
2471
|
+
3. **Lifecycle-aware collection** - Use \`collectAsStateWithLifecycle()\`
|
|
2472
|
+
4. **Modifier parameter** - Always accept Modifier as last parameter
|
|
2473
|
+
5. **Preview annotations** - Add @Preview for rapid iteration
|
|
2474
|
+
`,
|
|
2475
|
+
isNew: true
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
function generateAndroidViewsSkill() {
|
|
2479
|
+
return {
|
|
2480
|
+
type: "skill",
|
|
2481
|
+
path: ".claude/skills/android-views-patterns.md",
|
|
2482
|
+
content: `---
|
|
2483
|
+
name: android-views-patterns
|
|
2484
|
+
description: Android XML views and traditional patterns
|
|
2485
|
+
globs:
|
|
2486
|
+
- "**/*.kt"
|
|
2487
|
+
- "**/*.xml"
|
|
2488
|
+
---
|
|
2489
|
+
|
|
2490
|
+
# Android Views Patterns
|
|
2491
|
+
|
|
2492
|
+
## Activity Structure
|
|
2493
|
+
|
|
2494
|
+
\`\`\`kotlin
|
|
2495
|
+
class MainActivity : AppCompatActivity() {
|
|
2496
|
+
|
|
2497
|
+
private lateinit var binding: ActivityMainBinding
|
|
2498
|
+
private val viewModel: MainViewModel by viewModels()
|
|
2499
|
+
|
|
2500
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
2501
|
+
super.onCreate(savedInstanceState)
|
|
2502
|
+
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
2503
|
+
setContentView(binding.root)
|
|
2504
|
+
|
|
2505
|
+
setupUI()
|
|
2506
|
+
observeViewModel()
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
private fun setupUI() {
|
|
2510
|
+
binding.recyclerView.apply {
|
|
2511
|
+
layoutManager = LinearLayoutManager(this@MainActivity)
|
|
2512
|
+
adapter = userAdapter
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
binding.swipeRefresh.setOnRefreshListener {
|
|
2516
|
+
viewModel.refresh()
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
private fun observeViewModel() {
|
|
2521
|
+
lifecycleScope.launch {
|
|
2522
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
2523
|
+
viewModel.uiState.collect { state ->
|
|
2524
|
+
updateUI(state)
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
private fun updateUI(state: MainUiState) {
|
|
2531
|
+
binding.swipeRefresh.isRefreshing = state.isLoading
|
|
2532
|
+
userAdapter.submitList(state.users)
|
|
2533
|
+
binding.errorText.isVisible = state.error != null
|
|
2534
|
+
binding.errorText.text = state.error
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
\`\`\`
|
|
2538
|
+
|
|
2539
|
+
## Fragment Pattern
|
|
2540
|
+
|
|
2541
|
+
\`\`\`kotlin
|
|
2542
|
+
class UserFragment : Fragment(R.layout.fragment_user) {
|
|
2543
|
+
|
|
2544
|
+
private var _binding: FragmentUserBinding? = null
|
|
2545
|
+
private val binding get() = _binding!!
|
|
2546
|
+
|
|
2547
|
+
private val viewModel: UserViewModel by viewModels()
|
|
2548
|
+
private val args: UserFragmentArgs by navArgs()
|
|
2549
|
+
|
|
2550
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
2551
|
+
super.onViewCreated(view, savedInstanceState)
|
|
2552
|
+
_binding = FragmentUserBinding.bind(view)
|
|
2553
|
+
|
|
2554
|
+
setupUI()
|
|
2555
|
+
observeViewModel()
|
|
2556
|
+
viewModel.loadUser(args.userId)
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
override fun onDestroyView() {
|
|
2560
|
+
super.onDestroyView()
|
|
2561
|
+
_binding = null
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
\`\`\`
|
|
2565
|
+
|
|
2566
|
+
## RecyclerView Adapter
|
|
2567
|
+
|
|
2568
|
+
\`\`\`kotlin
|
|
2569
|
+
class UserAdapter(
|
|
2570
|
+
private val onItemClick: (User) -> Unit
|
|
2571
|
+
) : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {
|
|
2572
|
+
|
|
2573
|
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
2574
|
+
val binding = ItemUserBinding.inflate(
|
|
2575
|
+
LayoutInflater.from(parent.context),
|
|
2576
|
+
parent,
|
|
2577
|
+
false
|
|
2578
|
+
)
|
|
2579
|
+
return ViewHolder(binding)
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
2583
|
+
holder.bind(getItem(position))
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
inner class ViewHolder(
|
|
2587
|
+
private val binding: ItemUserBinding
|
|
2588
|
+
) : RecyclerView.ViewHolder(binding.root) {
|
|
2589
|
+
|
|
2590
|
+
init {
|
|
2591
|
+
binding.root.setOnClickListener {
|
|
2592
|
+
onItemClick(getItem(adapterPosition))
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
fun bind(user: User) {
|
|
2597
|
+
binding.nameText.text = user.name
|
|
2598
|
+
binding.emailText.text = user.email
|
|
2599
|
+
Glide.with(binding.avatar)
|
|
2600
|
+
.load(user.avatarUrl)
|
|
2601
|
+
.circleCrop()
|
|
2602
|
+
.into(binding.avatar)
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
|
|
2607
|
+
override fun areItemsTheSame(oldItem: User, newItem: User) =
|
|
2608
|
+
oldItem.id == newItem.id
|
|
2609
|
+
|
|
2610
|
+
override fun areContentsTheSame(oldItem: User, newItem: User) =
|
|
2611
|
+
oldItem == newItem
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
\`\`\`
|
|
2615
|
+
|
|
2616
|
+
## XML Layout
|
|
2617
|
+
|
|
2618
|
+
\`\`\`xml
|
|
2619
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2620
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
2621
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2622
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
2623
|
+
android:layout_width="match_parent"
|
|
2624
|
+
android:layout_height="match_parent">
|
|
2625
|
+
|
|
2626
|
+
<com.google.android.material.appbar.MaterialToolbar
|
|
2627
|
+
android:id="@+id/toolbar"
|
|
2628
|
+
android:layout_width="0dp"
|
|
2629
|
+
android:layout_height="?attr/actionBarSize"
|
|
2630
|
+
app:title="Users"
|
|
2631
|
+
app:layout_constraintTop_toTopOf="parent"
|
|
2632
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
2633
|
+
app:layout_constraintEnd_toEndOf="parent" />
|
|
2634
|
+
|
|
2635
|
+
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
2636
|
+
android:id="@+id/swipeRefresh"
|
|
2637
|
+
android:layout_width="0dp"
|
|
2638
|
+
android:layout_height="0dp"
|
|
2639
|
+
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
2640
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
2641
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
2642
|
+
app:layout_constraintEnd_toEndOf="parent">
|
|
2643
|
+
|
|
2644
|
+
<androidx.recyclerview.widget.RecyclerView
|
|
2645
|
+
android:id="@+id/recyclerView"
|
|
2646
|
+
android:layout_width="match_parent"
|
|
2647
|
+
android:layout_height="match_parent" />
|
|
2648
|
+
|
|
2649
|
+
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
2650
|
+
|
|
2651
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
2652
|
+
\`\`\`
|
|
2653
|
+
|
|
2654
|
+
## ViewModel with Repository
|
|
2655
|
+
|
|
2656
|
+
\`\`\`kotlin
|
|
2657
|
+
@HiltViewModel
|
|
2658
|
+
class UserViewModel @Inject constructor(
|
|
2659
|
+
private val repository: UserRepository,
|
|
2660
|
+
private val savedStateHandle: SavedStateHandle
|
|
2661
|
+
) : ViewModel() {
|
|
2662
|
+
|
|
2663
|
+
private val _uiState = MutableStateFlow(UserUiState())
|
|
2664
|
+
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
|
|
2665
|
+
|
|
2666
|
+
fun loadUsers() {
|
|
2667
|
+
viewModelScope.launch {
|
|
2668
|
+
_uiState.update { it.copy(isLoading = true) }
|
|
2669
|
+
try {
|
|
2670
|
+
val users = repository.getUsers()
|
|
2671
|
+
_uiState.update { it.copy(users = users, isLoading = false) }
|
|
2672
|
+
} catch (e: Exception) {
|
|
2673
|
+
_uiState.update { it.copy(error = e.message, isLoading = false) }
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
data class UserUiState(
|
|
2680
|
+
val users: List<User> = emptyList(),
|
|
2681
|
+
val isLoading: Boolean = false,
|
|
2682
|
+
val error: String? = null
|
|
2683
|
+
)
|
|
2684
|
+
\`\`\`
|
|
2685
|
+
|
|
2686
|
+
## Best Practices
|
|
2687
|
+
|
|
2688
|
+
1. **View Binding** - Use over findViewById or synthetic imports
|
|
2689
|
+
2. **Lifecycle awareness** - Collect flows in repeatOnLifecycle
|
|
2690
|
+
3. **ListAdapter** - For efficient RecyclerView updates
|
|
2691
|
+
4. **Navigation Component** - For fragment navigation
|
|
2692
|
+
5. **Clean up bindings** - Set to null in onDestroyView
|
|
2693
|
+
`,
|
|
2694
|
+
isNew: true
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
1662
2697
|
function generateIterativeDevelopmentSkill(stack) {
|
|
1663
2698
|
const testCmd = getTestCommand(stack);
|
|
1664
2699
|
const lintCmd = getLintCommand(stack);
|
|
@@ -2811,6 +3846,7 @@ function formatLanguage(lang) {
|
|
|
2811
3846
|
}
|
|
2812
3847
|
function formatFramework(fw) {
|
|
2813
3848
|
const names = {
|
|
3849
|
+
// JavaScript/TypeScript Frontend
|
|
2814
3850
|
nextjs: "Next.js",
|
|
2815
3851
|
react: "React",
|
|
2816
3852
|
vue: "Vue.js",
|
|
@@ -2822,30 +3858,50 @@ function formatFramework(fw) {
|
|
|
2822
3858
|
remix: "Remix",
|
|
2823
3859
|
gatsby: "Gatsby",
|
|
2824
3860
|
solid: "Solid.js",
|
|
3861
|
+
// JavaScript/TypeScript Backend
|
|
2825
3862
|
express: "Express",
|
|
2826
3863
|
nestjs: "NestJS",
|
|
2827
3864
|
fastify: "Fastify",
|
|
2828
3865
|
hono: "Hono",
|
|
2829
3866
|
elysia: "Elysia",
|
|
2830
3867
|
koa: "Koa",
|
|
3868
|
+
// Python
|
|
2831
3869
|
fastapi: "FastAPI",
|
|
2832
3870
|
django: "Django",
|
|
2833
3871
|
flask: "Flask",
|
|
2834
3872
|
starlette: "Starlette",
|
|
3873
|
+
// Go
|
|
2835
3874
|
gin: "Gin",
|
|
2836
3875
|
echo: "Echo",
|
|
2837
3876
|
fiber: "Fiber",
|
|
3877
|
+
// Rust
|
|
2838
3878
|
actix: "Actix",
|
|
2839
3879
|
axum: "Axum",
|
|
2840
3880
|
rocket: "Rocket",
|
|
3881
|
+
// Ruby
|
|
2841
3882
|
rails: "Rails",
|
|
2842
3883
|
sinatra: "Sinatra",
|
|
3884
|
+
// Java/Kotlin
|
|
2843
3885
|
spring: "Spring",
|
|
2844
3886
|
quarkus: "Quarkus",
|
|
3887
|
+
// Android
|
|
3888
|
+
"jetpack-compose": "Jetpack Compose",
|
|
3889
|
+
"android-views": "Android Views",
|
|
3890
|
+
room: "Room",
|
|
3891
|
+
hilt: "Hilt",
|
|
3892
|
+
"ktor-android": "Ktor",
|
|
3893
|
+
// Swift/iOS
|
|
3894
|
+
swiftui: "SwiftUI",
|
|
3895
|
+
uikit: "UIKit",
|
|
3896
|
+
vapor: "Vapor",
|
|
3897
|
+
swiftdata: "SwiftData",
|
|
3898
|
+
combine: "Combine",
|
|
3899
|
+
// CSS/UI
|
|
2845
3900
|
tailwind: "Tailwind CSS",
|
|
2846
3901
|
shadcn: "shadcn/ui",
|
|
2847
3902
|
chakra: "Chakra UI",
|
|
2848
3903
|
mui: "Material UI",
|
|
3904
|
+
// Database/ORM
|
|
2849
3905
|
prisma: "Prisma",
|
|
2850
3906
|
drizzle: "Drizzle",
|
|
2851
3907
|
typeorm: "TypeORM",
|
|
@@ -3237,6 +4293,13 @@ main().catch((err) => {
|
|
|
3237
4293
|
process.exit(1);
|
|
3238
4294
|
});
|
|
3239
4295
|
export {
|
|
4296
|
+
createTaskFile,
|
|
4297
|
+
formatFramework2 as formatFramework,
|
|
4298
|
+
formatLanguage2 as formatLanguage,
|
|
3240
4299
|
getVersion,
|
|
3241
|
-
parseArgs
|
|
4300
|
+
parseArgs,
|
|
4301
|
+
promptNewProject,
|
|
4302
|
+
showBanner,
|
|
4303
|
+
showHelp,
|
|
4304
|
+
showTechStack
|
|
3242
4305
|
};
|