Google’s Flutter framework is rapidly gaining popularity among developers for enabling the creation of beautiful, natively-compiled applications for mobile, web, and desktop from a single codebase. However, the abundance of Flutter tutorials focused on editors like VSCode and Android Studio can be discouraging for developers who use or are interested in Neovim.
Let’s put that to an end.
Why Neovim?
Neovim is the terminal text editor that might finally earn your engineer father’s approval. It’s a modern take on Vim, a highly configurable and efficient editor celebrated for its powerful modal editing capabilities and mouse-free philosophy.
Between its lightweight, resource efficient nature and keyboard-centric workflow, Neovim can easily make Flutter’s already fast UI workflow blazingly fast.
It’s also written and configured in Lua.
The setup
To grant Neovim all the bells and whistles of a VSCode Flutter experience, we’ll take advantage of two key plugins: dart-vim-plugin and flutter-tools.nvim.
Note: This tutorial assumes you have a basic Neovim configuration in place. For help setting Neovim up to this point, consider using kickstart.nvim to get a basic IDE experience set up.
dart-vim-plugin
dart-vim-plugin
adds important Dart language support to Vim. It offers features such as syntax highlighting, code indentation, and basic code completion.
Start by importing the plugin via your preferred plugin manager, I use lazy.nvim:
require("lazy").setup({
// ... Among your other plugins
"dart-lang/dart-vim-plugin",
})
We can now enable this plugin’s features via the following global variables:
vim.g.dart_style_guide = 2
vim.g.dart_format_on_save = 1
vim.g.dart_trailing_comma_indent = true
dart_style_guide
sets the tab width used when auto-formatting. In our case, we mimic the VSCode experience by setting it to two spaces.
dart_format_on_save
is self-explanatory; it triggers the Dart auto-formatter upon writing the buffer.
dart_trailing_comma_indent
, my favorite, ensures proper indentation when adding a trailing comma to comma-separated lists like function parameters and argument listings.
flutter-tools.nvim
flutter-tools.nvim
is a plugin that provides all the functionality we know and love from the Flutter platform:
hot reload/restart, device management, debugger support, and integration with the Dart analysis server for real-time code analysis.
The setup for this plugin is a bit more involved, but nothing we can’t handle.
Just like before, we first add the plugin:
require("lazy").setup({
// ... Among your other plugins
"dart-lang/dart-vim-plugin",
{
"akinsho/flutter-tools.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
"stevearc/dressing.nvim",
},
},
})
The plugin depends on plenary.nvim and dressing.nvim, so make sure to include them as dependencies
.
Because flutter-tools.nvim
directly works with the Dart language server, we can (and should) avoid configuring dartls in our lspconfig setup.
For a near-VSCode experience, setup the plugin with the following snippet:
require("flutter-tools").setup({
decorations = {
statusline = {
app_version = true,
device = true,
},
},
widget_guides = {
enabled = true,
},
closing_tags = {
highlight = "Comment",
prefix = "//",
enabled = true,
},
lsp = {
color = {
enabled = true,
background = true,
foreground = false,
virtual_text = true,
virtual_text_str = "■",
},
settings = {
showTodos = true,
completeFunctionCalls = true,
enableSnippets = true,
},
},
})
decorations
controls what information is shown in the buffer status line.
widget_guides
draws the widget guide lines indicating the parent-child relationships within the buffer. While still experimental, this feature can come in handy!
closing_tags
determines the appearance of closing tags - the “comments” after the closing braces of a widget. This setup ensures the ‘Comment’ highlight group is used when rendering them.
lsp
describes how the built-in Dart LSP configuration behaves. In color we’ve enabled virtual text messages for warnings, syntax errors, etc. And in settings, we’re enabling features like TODO diagnostics, auto-completing function calls with required parameters, and including snippets like
stless -> StatelessWidget…
in code completion.
With that configured, we now have access to some useful commands, only four of which we really need to know for now:
:FlutterRun
Starts the Flutter application in the current directory
:FlutterQuit
Quits the currently running Flutter application
:FlutterReload
Performs a Hot Reload on the currently running app
:FlutterRestart
Performs a Hot Restart on the currently running app
Now while you can type out these commands whenever you need them, part of the Neovim fun is turning things into convenient keybindings:
vim.keymap.set("n", "<leader>ff", ":FlutterRun<CR>")
vim.keymap.set("n", "<leader>fq", ":FlutterQuit<CR>")
vim.keymap.set("n", "<leader>fr", ":FlutterReload<CR>")
vim.keymap.set("n", "<leader>fR", ":FlutterRestart<CR>")
Debugging
If you’re like me and exclusively develop in Flutter using a debugger, I’ve got you covered. To achieve this, we’ll need the help of two more plugins:
nvim-dap and nvim-dap-ui.
By connecting these two plugins to our beloved flutter-tools.nvim
we can achieve a pretty smooth Flutter debugging experience:
Start by configuring the plugins, we’ll take it straight from their README:
require("dapui").setup()
local dap, dapui = require("dap"), require("dapui")
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open()
end
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close()
end
dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close()
end
local sign = vim.fn.sign_define
sign("DapBreakpoint", { text = "●", texthl = "DapBreakpoint", linehl = "", numhl = ""})
sign("DapBreakpointCondition", { text = "●", texthl = "DapBreakpointCondition", linehl = "", numhl = ""})
sign("DapLogPoint", { text = "◆", texthl = "DapLogPoint", linehl = "", numhl = ""})
vim.keymap.set("n", "<leader>db", ":lua require('dap').toggle_breakpoint()<CR>")
vim.keymap.set("n", "<leader>dB", ":lua require('dap').set_breakpoint(vim.fn.input("Breakpoint Condition: "))<CR>")
vim.keymap.set("n", "<leader>dd", ":lua require('dap').continue()<CR>")
vim.keymap.set("n", "<leader>do", ":lua require('dap').step_over()<CR>")
vim.keymap.set("n", "<leader>di", ":lua require('dap').step_into()<CR>")
Then enable debugging via dap in our flutter-tools.nvim
configuration:
require("flutter-tools").setup {
decorations = {
statusline = {
app_version = true,
device = true,
},
},
widget_guides = {
enabled = false,
},
closing_tags = {
highlight = "Comment",
prefix = "//",
enabled = true,
},
dev_log = {
enabled = false,
},
lsp = {
color = {
enabled = true,
background = true,
foreground = false,
virtual_text = true,
virtual_text_str = "■",
},
settings = {
showTodos = false,
completeFunctionCalls = true,
enableSnippets = true,
},
},
debugger = {
enabled = true,
run_via_dap = true,
exception_breakpoints = {},
register_configurations = function(_)
require("dap").configurations.dart = {}
require("dap.ext.vscode").load_launchjs()
end,
},
}
Dap integration is enabled by including the debugger
map in our configuration object.
As a bonus, this setup allows us to use our pre-existing VSCode launch.js
configurations from within Neovim. Running the :FlutterRun
command, or simply using our new <leader>ff keybinding, will display a list of our VSCode run configurations to select from.
Lastly, the nvim-dap
README provides some handy keybindings to navigate the debugger - things like breakpoint management and code traversal:
vim.keymap.set("n", "<leader>db", ":lua require('dap').toggle_breakpoint()<CR>")
vim.keymap.set("n", "<leader>dB", ":lua require('dap').set_breakpoint(vim.fn.input('Breakpoint Condition: '))<CR>")
vim.keymap.set("n", "<leader>dd", ":lua require('dap').continue()<CR>")
vim.keymap.set("n", "<leader>do", ":lua require('dap').step_over()<CR>")
vim.keymap.set("n", "<leader>di", ":lua require('dap').step_into()<CR>")
Conclusion
When I began my Flutter career, I was afraid I’d be permanently forced to work in VSCode and ditch my naive fantasy of living in the terminal.
However, thanks to the rich community overlap between Flutter and Neovim, I was able to smoothly transition from VSCode to Neovim with just four plugins. And now so can you!