{"id":13783978,"url":"https://github.com/ubuntu/user_manager","last_synced_at":"2025-05-12T18:54:13.734Z","repository":{"id":83295203,"uuid":"369639862","full_name":"ubuntu/user_manager","owner":"ubuntu","description":"A tutorial for creating an Ubuntu Linux Flutter app, using the yaru theme","archived":false,"fork":false,"pushed_at":"2021-09-23T07:42:48.000Z","size":1256,"stargazers_count":27,"open_issues_count":0,"forks_count":6,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-20T15:44:29.291Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ubuntu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-05-21T19:50:55.000Z","updated_at":"2024-12-15T14:34:11.000Z","dependencies_parsed_at":"2023-07-01T15:45:45.927Z","dependency_job_id":null,"html_url":"https://github.com/ubuntu/user_manager","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubuntu%2Fuser_manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubuntu%2Fuser_manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubuntu%2Fuser_manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ubuntu%2Fuser_manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ubuntu","download_url":"https://codeload.github.com/ubuntu/user_manager/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253804289,"owners_count":21967046,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T19:00:33.985Z","updated_at":"2025-05-12T18:54:13.695Z","avatar_url":"https://github.com/ubuntu.png","language":"Dart","funding_links":[],"categories":["Projects"],"sub_categories":[],"readme":"# Building a Yaru app with Flutter\n\n|||\n|---|---|\n|**Summary**||\n|**URL**|https://github.com/ubuntu/user_manager|\n|**Category**||\n|**Environment**|Linux|\n|**Status**||\n|**Feedback Link**||\n|**Author**|Frederik Feichtmeier|\n\n____\n\n# Introduction\n\n## What you'll learn\n\n- Setting up a Flutter development environment in Ubuntu\n- Building a Flutter app for the Linux desktop, using the [yaru theme](https://github.com/ubuntu/yaru.dart)\n- Using [json-server](https://github.com/typicode/json-server) as a local backend to store data\n\n## What you'll build\n\n![](.github/09_final_app_dark.png)\n\n____\n\n# Set up your Flutter environment\n\n## Install Flutter on Ubuntu and enable flutter Linux desktop support\n\n```bash\nsudo snap install flutter --classic\nflutter channel beta\nflutter upgrade\nflutter config --enable-linux-desktop\n```\n\n## Install and setup Visual Studio Code\n\nTo install VS Code in Ubuntu fire up a terminal and run the following command:\n\n```bash\nsudo snap install code --classic\n```\n\nLaunch VS Code Quick Open (Ctrl+P), paste the following command ...\n\n```\next install Dart-Code.flutter\n```\n\n... and press enter to install [the official Flutter VS Code extension](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter).\n\n# Get started\n\nOpen a terminal or the VS Code integrated terminal and run the following command to create a new flutter project for the fictional organization `org.flutterfans`\n\n```bash\nflutter create --org org.flutterfans user_manager\n```\n\nOpen the project directory `user_manager` you've just created with VS Code.\n\n![](.github/00_open.png)\n\nYou should now see the following directories (1) and the \"Linux (linux-x64)\" (2) device in use, which is the default on Linux after you've enabled the linux desktop support:\n\n![directory_overview](.github/01_directory_overview.png)\n\nLet's make a small test if the application launches, by opening the `lib` directory (1) and clicking on the small `Run` label above `void main()` (2) \n\n![](.github/02_first_launch.png)\n\n## Using the Yaru theme\n\nOpen `pubspec.yaml` - the file for managing of the lifecycle of your app - and add `yaru: ^0.0.6` under the dependencies section, so you end up with the following dependencies (check that `yaru` is on the same column as `flutter`): \n\n```yaml\ndependencies:\n  flutter:\n    sdk: flutter\n  yaru: ^0.0.6\n```\n\nYou can now import the yaru theme by adding \n\n```dart\nimport 'package:yaru/yaru.dart' as yaru;\n```\nat the top of `main.dart`. Use the yaru theme in your app by setting the `theme` property of `MaterialApp` to `yaru.lightTheme` and the `darkTheme` property to `yaru.darkTheme` so you end up with the following class definition - your app now follows your system's theme.\n\n```dart\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Flutter Demo',\n      theme: yaru.lightTheme,\n      darkTheme: yaru.darkTheme,\n      home: MyHomePage(title: 'Flutter Demo Home Page'),\n    );\n  }\n}\n```\n\n![](.github/04_toggle_themes.gif)\n\n# Building the app\n\n## Organize your project structure\n\nYou might want to organize your project's directory structure a little bit. Start by creating a `view` directory under `lib` by right-clicking the `lib` directory and click the `New folder` menu entry and type in `view` into the textfield that pops out.\n\n![](.github/05_create_view_dir.png)\n\nRepeat this step with a `model` and a `service` directory to end up with the following directory tree above `main.dart`:\n\n![](.github/06_project_structure.png)\n\n## A simple user model\n\nTo easily send user data across your app and to the server you want to have a user class, containing only the data properties of your user.\n\nRight-click on the `model` directory and click on `New file` and name it `user.dart`:\n\n![](.github/07_new_file.png)\n\nInsert the following code\n\n```dart\nclass User {\n  int? id;\n  String name;\n\n  User({required this.id, required this.name});\n\n  User.empty() : this(id: null, name: \"\");\n\n  factory User.fromJson(Map\u003cString, dynamic\u003e json) =\u003e\n      User(id: json['id'], name: json['name']);\n\n  Map\u003cString, dynamic\u003e toJson() =\u003e {\"id\": id, \"name\": name};\n}\n```\n\n## The user service\n\nTo let your app communicate via HTTP with your server, add `http: ^0.13.1` in your `pubspec.yaml` under the `dependencies` section, create `user_service.dart` in the `service` directory and paste the following code into:\n\n```dart\nimport 'dart:convert';\n\nimport 'package:http/http.dart' as http;\nimport 'package:user_manager/model/user.dart';\n\nclass UserService {\n  UserService({required this.uri});\n\n  final String uri;\n\n  Future\u003cList\u003cUser\u003e\u003e getUsers() async {\n    final response = await http.get(Uri.http(uri, 'users'));\n\n    if (response.statusCode == 200) {\n      var usersJson = jsonDecode(response.body.toString()) as List;\n      return usersJson.map((json) =\u003e User.fromJson(json)).toList();\n    } else {\n      throw Exception(response.statusCode.toString() + ': ' + response.body);\n    }\n  }\n\n  Future saveUser(User user) async {\n    for (var aUser in await getUsers()) {\n      if (aUser.id == user.id) {\n        await updateUser(user);\n        return;\n      }\n    }\n    await addUser(user);\n  }\n\n  Future\u003chttp.Response\u003e addUser(User user) async {\n    return await http.post(Uri.http(uri, 'users'),\n        headers: \u003cString, String\u003e{\n          'Content-Type': 'application/json; charset=UTF-8',\n        },\n        body: jsonEncode(user.toJson()));\n  }\n\n  Future updateUser(User user) async {\n    return await http.put(Uri.http(uri, 'users/${user.id}'),\n        headers: \u003cString, String\u003e{\n          'Content-Type': 'application/json; charset=UTF-8',\n        },\n        body: jsonEncode(user.toJson()));\n  }\n\n  Future removeUser(User user) async {\n    await http.delete((Uri.http(uri, 'users/${user.id}')));\n  }\n}\n```\n\n## The UI\n\n### One card for each user\n\nEach user that will be managed is represented by a card inside the UI.\n\nCreate `user_card.dart` in the `view` directory and paste in the following code\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:user_manager/model/user.dart';\n\nclass UserCard extends StatefulWidget {\n  final User user;\n  final VoidCallback onDelete;\n  final VoidCallback onEdit;\n\n  UserCard({required this.user, required this.onDelete, required this.onEdit});\n\n  @override\n  _UserCardState createState() =\u003e _UserCardState();\n}\n\nclass _UserCardState extends State\u003cUserCard\u003e {\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: \u003cWidget\u003e[\n          ListTile(\n            leading: Icon(Icons.person),\n            title: Text(widget.user.name),\n          ),\n          Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: \u003cWidget\u003e[\n              TextButton(\n                child: const Text('Edit'),\n                onPressed: () =\u003e widget.onEdit(),\n              ),\n              const SizedBox(width: 8),\n              TextButton(\n                child: const Text('Remove'),\n                onPressed: () =\u003e widget.onDelete(),\n              ),\n              const SizedBox(width: 8),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n```\n\n### A dialog to edit users in\n\nA user will be edited inside a dialog, spawned from clicking on the either the big green floating action button or the edit button of your user card.\n\nCreate the file `user_edit_dialog.dart` inside the `view` directory and copy/paste the following code into:\n\n\n```dart\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\n\nclass UserEditDialog extends StatefulWidget {\n  final TextEditingController nameController;\n  final AsyncCallback editUser;\n\n  UserEditDialog({required this.nameController, required this.editUser});\n\n  @override\n  _UserEditDialogState createState() =\u003e _UserEditDialogState();\n}\n\nclass _UserEditDialogState extends State\u003cUserEditDialog\u003e {\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: Container(\n        height: 150,\n        width: 200,\n        child: Padding(\n          padding: const EdgeInsets.all(10.0),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.start,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Padding(\n                padding: const EdgeInsets.all(8.0),\n                child: TextField(\n                  autofocus: true,\n                  controller: widget.nameController,\n                  decoration: InputDecoration(hintText: 'User name'),\n                ),\n              ),\n              Row(\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.all(8.0),\n                    child: ElevatedButton(\n                      onPressed: () async =\u003e\n                          Navigator.of(context).pop(widget.editUser()),\n                      child: Text(\n                        \"Save\",\n                        style: TextStyle(color: Colors.white),\n                      ),\n                    ),\n                  ),\n                  Padding(\n                    padding: const EdgeInsets.all(8.0),\n                    child: TextButton(\n                      onPressed: () =\u003e Navigator.of(context).pop(),\n                      child: Text(\"Cancel\"),\n                    ),\n                  )\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n\n### The new home page\n\nThe old home page won't be enough anymore so you need a new one to load the user cards into. The home page uses the user service to manage the user data.\n\nOpen your `pubspec.yaml` file again and add `injector: ^2.0.0` in the `dependencies` section. Create the file `home_page.dart` in the `view` directory and paste the following code:\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:injector/injector.dart';\nimport 'package:user_manager/model/user.dart';\nimport 'package:user_manager/service/user_service.dart';\nimport 'package:user_manager/view/user_card.dart';\nimport 'package:user_manager/view/user_edit_dialog.dart';\n\nclass HomePage extends StatefulWidget {\n  @override\n  _HomePageState createState() =\u003e _HomePageState();\n}\n\nclass _HomePageState extends State\u003cHomePage\u003e {\n  late TextEditingController _nameController;\n  final _userService = Injector.appInstance.get\u003cUserService\u003e();\n\n  @override\n  void initState() {\n    _nameController = TextEditingController();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Home'),\n      ),\n      body: Center(\n        child: Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: Scrollbar(\n            child: FutureBuilder(\n                future: _userService.getUsers(),\n                builder: (context, AsyncSnapshot\u003cList\u003cUser\u003e\u003e snapShot) {\n                  if (snapShot.hasData) {\n                    return ListView(\n                      children: snapShot.data!\n                          .map((user) =\u003e UserCard(\n                              user: user,\n                              onDelete: () {\n                                _userService\n                                    .removeUser(user)\n                                    .then((value) =\u003e setState(() {\n                                          _showSnackBar(\n                                              'Deleted user: ' + user.name);\n                                        }))\n                                    .onError((error, stackTrace) =\u003e\n                                        _showSnackBar(error.toString()));\n                              },\n                              onEdit: () =\u003e editUser(user, context)))\n                          .toList(),\n                    );\n                  }\n                  return CircularProgressIndicator();\n                }),\n          ),\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        child: Icon(Icons.add),\n        onPressed: () {\n          _nameController.text = \"\";\n          editUser(new User.empty(), context);\n        },\n      ),\n    );\n  }\n\n  void editUser(User user, BuildContext context) {\n    _nameController.text = user.name;\n    showDialog(\n        context: context,\n        barrierDismissible: false,\n        builder: (BuildContext context) {\n          return UserEditDialog(\n              nameController: _nameController,\n              editUser: () async {\n                user.name = _nameController.text;\n                _userService\n                    .saveUser(user)\n                    .then((value) =\u003e setState(() {\n                          _showSnackBar('Saved user: ' + user.name);\n                        }))\n                    .onError(\n                        (error, stackTrace) =\u003e _showSnackBar(error.toString()));\n              });\n        });\n  }\n\n  void _showSnackBar(String message) {\n    ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(duration: const Duration(seconds: 1), content: Text(message)));\n  }\n}\n```\n\n## Finish up your main.dart\n\nThe UI of the test app is no longer needed. Remove all Widgets except `MyApp`, change the `home` property to `HomePage()`, change your `main()` function to by asynchronous, returning a `Future\u003cvoid\u003e` and register your `UserService` with the `Injector` class before you run your app. To be sure you've imported everything correctly, replace all code inside `main.dart` with the following new version:\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:injector/injector.dart';\nimport 'package:user_manager/service/user_service.dart';\nimport 'package:user_manager/view/home_page.dart';\nimport 'package:yaru/yaru.dart' as yaru;\n\nFuture\u003cvoid\u003e main() async {\n  final userService = UserService(uri: 'localhost:3000');\n  Injector.appInstance.registerDependency\u003cUserService\u003e(() =\u003e userService);\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Flutter Demo',\n      theme: yaru.lightTheme,\n      darkTheme: yaru.darkTheme,\n      home: HomePage(),\n    );\n  }\n}\n```\n\n## Create a local server to process client requests\n\n### Create mock data\n\nCreate `db.json` inside the root folder of your application and insert some fictional users in the json format:\n\n```json\n{\n    \"users\": [\n        {\n            \"id\": 1,\n            \"name\": \"Carlo\"\n        },\n        {\n            \"id\": 2,\n            \"name\": \"Mads\"\n        },\n        {\n            \"id\": 3,\n            \"name\": \"Frederik\"\n        },\n        {\n            \"id\": 4,\n            \"name\": \"Stuart\"\n        },\n        {\n            \"id\": 5,\n            \"name\": \"Paul\"\n        },\n        {\n            \"id\": 6,\n            \"name\": \"Muq\"\n        }\n    ]\n}\n```\n\n### Install node-js and json-server\n\nWe will use a [fantastic local mock server](https://github.com/typicode/json-server) to let our client talk to. To do so we need the [node snap](https://snapcraft.io/node). Enter the following commands to install node and json-server.\n\n```bash\nsudo snap install node --classic\nsudo npm install -g json-server\n```\n\n# Entering the user management :)\n\nStart your json-server by running the following command\n\n```bash\njson-server --watch db.json\n```\n\nStart your app again! Done :) You can now manage users inside a Ubuntu Linux app, using the yaru theme:\n\n![](.github/08_final_app.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fubuntu%2Fuser_manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fubuntu%2Fuser_manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fubuntu%2Fuser_manager/lists"}