Simple mobile keyboard toolbar for Flutter applications.

Simple mobile keyboard toolbar for flutter.

Other awesome & popular flutter keyboard packages:

Unlike math_keyboard or Casey Henson's implementation, flutter_smkt like keyboard_actions, simply puts a toolbar on top of the system keyboard.

If you would like to replace the system keyboard with your own custom keyboard please reference the other packages.


The name flutter simple mobile keyboard toolbar was chosen as a commitment device, not just for the developers, but for the broader open source community.

So if you can make it simpler or better or greater, please let us know! Pull requests are welcome.


Getting started

flutter_smkt: ^0.0.2

Usage

Set up

Wrap the main app widget (or the widget that needs the toolbar) with the KeyboardToolbar widget. However KeyboardToolbar relies on MediaQuery so need to make sure a global platform widget such as MaterialApp, CupertinoApp, or PlatformApp is an ancestor of KeyboardToolbar.

import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:flutter_smkt/flutter_smkt.dart';

void main() {
  
   runApp(
     PlatformApp(
        home: KeyboardToolbar(
          child: MainApp()
        ),
     )
   );
}

Reference the Troubleshooting section for additional debugging related information.

Example using DoneButton

Then for example can use the toolbar with a DoneButton (from flutter_smkt) for a multiline TextField widget where the system keyboard would only have a 'Newline Button' but not a 'Done' button, such as:

import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:flutter_smkt/flutter_smkt.dart';
import 'package:flutter/widgets.dart';

typedef EditOnChangedCallback = void Function(String text);

class MyApp extends StafulWidget {
  
  @override
  State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  
  bool editMode = false;
  
  String text = 'Hi';
  String? editedText;

  
  void editOnChangedCallback(String text) => setState(() => editedText = text);
  void onPressedCallback() {
    setState(() {
      text = editText;
      editMode = false;
    });
  }
   
  @override
  Widget build(BuildContext context) {
    if (editMode) 
      return _TextInput(
        initialText: text,
        editOnChangedCallback: editOnChangedCallback,
        onPress: onPressedCallback
      );
    
    return Row(
       children: [
         PlatformText(text),
          PlatformButton(
             child: PlatformText('EDIT'),
            onPressed: () => setState(() => editMode = true)
          )
       ]
    );
  }
}


class _TextInput extends StatefulWidget {
  final String initialText;
  final EditOnChangedCallback editOnChangedCallback;
  final OnToolbarButtonPressed onPress; // OnToolbarButtonPressed typedef provided by fsmkt
  
  _TextInput({
     required this.initialText,
     required this.EditOnChangedCallback,
     required this.onPress
  });

  @override
  State<StatefulWidget> createState() => _TextInputState();
}

class _TextInputState extends State<_TextInput> {

   late TextEditingController _controller;
  
   final String initialText;
   final EditOnChangedCallback editOnChangedCallback;
   final OnToolbarButtonPressed onPress; // OnToolbarButtonPressed typedef provided by fsmkt

   _TextInputState({
      required this.initialText,
      required this.EditOnChangedCallback,
      required this.onPress
   });
  
   // Need initialize a controller for interaction & the toolbar
   @override
   void initState() {
      super.initState();

      // initialize the TextEditingController
      _controller = TextEditingController(text: initialText);
      _controller.addListener(() => editOnChangedCallback(_controller.text));

      // Add a frame callback to show toolbar
      WidgetsBinding.instance!
        .addPostFrameCallback(
          (_) {
           KeyboardToolbar.of(context).makeVisible(
             toolbarButtons: [
                DoneButton(onPressed)
             ]
           );
        }
      );
   }

   @override
   void dispose() {
      _controller.dispose();
      super.dispose();
   }

   @override
   Widget build(BuildContext context) {
     return PlatformTextFormField(
        autofocus: true,
        controller: _controller,
        onFieldSubmitted: ,
        maxLines: null,
        validator: validator,
        inputFormatters: inputFormatters,
     );
   }
}

The keyboard toolbar gets built by this step, and is hidden right after onPressed is called in the DoneButton widget, with the call KeyboardToolbar.of(context).hide().

// Add a frame callback to show toolbar
WidgetsBinding.instance!
  .addPostFrameCallback(
    (_) {
     KeyboardToolbar.of(context).makeVisible(
       toolbarButtons: [
          DoneButton(onPressed)
       ]
     );
  }
);

Custom toolbar buttons

By extending the onPress callback you can use a custom DoneButton too.

// Add a frame callback to show toolbar
WidgetsBinding.instance!
  .addPostFrameCallback(
    (_) {
      KeyboardToolbar.of(context).makeVisible(
        toolbarButtons: [
          PlatformTextButton(
            child: PlatformText('Done'),
            onPressed: () {
               onPressed();
               KeyboardToolbar.of(context).hide();
            }
          )   
        ]
      );
    }
  );

Custom toolbar view

Similarly you can extend KeyboardToolbarView to customize the whole toolbar view:

void main() {
   runApp(
     PlatformApp(
        home: KeyboardToolbar(
          child: MainApp(),
          toolbarViewBuilder: () => CustomToolbarView()
        ),
     )
   );
}

class CustomToolbarView extends KeyboardToolbarView {

   final List<Widget> toolbarButtons;

   const CustomToolbarView({required this.toolbarButtons}):
     super(toolbarButtons: toolbarButtons);

   @override
   Widget build(BuildContext context) {
      return Container(
         color: Colors.white,
         child: Row(
            children: [PlatformText('ʕ·͡ᴥ·ʔ'), ...toolbarButtons],
            mainAxisAlignment: MainAxisAlignment.end,
         ),
      );
   }
}

Where CustomToolbarView extends KeyboardToolbarView

Troubleshooting

inactive InputConnection

Currently the following lines are printed when the toolbar is activated. Don't panic it's not an error on your end, the messages seem to be generated for brief time period when the toolbar is being built.

W/IInputConnectionWrapper(32148): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper(32148): getTextBeforeCursor on inactive InputConnection
W/IInputConnectionWrapper(32148): getTextAfterCursor on inactive InputConnection
W/IInputConnectionWrapper(32148): getSelectedText on inactive InputConnection
W/IInputConnectionWrapper(32148): endBatchEdit on inactive InputConnection
W/IInputConnectionWrapper(32148): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper(32148): endBatchEdit on inactive InputConnection
W/IInputConnectionWrapper(32148): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper(32148): endBatchEdit on inactive InputConnection

Please let us know if you can fix this.

TextField's view is hidden below the toolbar's view.

TextField actually hidden below the virtual keyboard not the toolbar.

If the TextField is hidden below the virtual keyboard without the toolbar.

Then try:

  • Wrapping the whole view in a Scaffold with resizeToAvoidBottomInset property.
  • Adding a ListView allowing the user to scroll up.

Reference:

`TextField actually hidden by the toolbar's view.

The KeyboardToolbar class exposes the toolbarHeight as a stream as KeyboardToolbar.of(context).toolbarHeight.

The toolbarHeight stream can then be used with a StreamBuilder (& ListView or ListView.builder) to create additional padding, allowing the user to scroll and access the TextField.

For example can wrap the originalView with the TextField as:

Widget belowToolbarPadding = StreamBuilder(
  stream: KeyboardToolbar.of(context).toolbarHeight,
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    if (!snapshot.hasData) return Container();  
    return Padding(padding: EdgeInsets.only(bottom: snapshot.data));
  }
);

Widget wrappedOriginalView = ListView(
  children: const <Widget>[
    originalView,
    belowToolbarPadding
  ]
);

Note: It is very necessary to provide the Padding separately to avoid rebuilding the originalView on toolbarHeight change.

Let us know if there is a better solution.

Additional information

The name 'flutter_simple_mobile_keyboard_toolbar' was chosen as a commitment device, not just for the developers, but for the broader open source community.

So if you can make it simpler or better or greater, please let us know! Pull requests are welcome.

Libraries

flutter_smkt