Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active June 19, 2022 11:15
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save slightfoot/f6e5cf0a0705f2d967ec9745e741cbf7 to your computer and use it in GitHub Desktop.
Save slightfoot/f6e5cf0a0705f2d967ec9745e741cbf7 to your computer and use it in GitHub Desktop.
Recreating Android Single-Top Activity behaviour with Flutter Route's - by Simon Lightfoot
// MIT License
//
// Copyright (c) 2019 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/material.dart';
void main() => runApp(ExampleApp());
class ExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
onGenerateRoute: (RouteSettings settings) {
if (settings.name == Navigator.defaultRouteName) {
return Activity<int>(
builder: (BuildContext context) => TestScreen(index: 0),
intent: 0,
);
}
return null;
},
);
}
}
class TestScreen extends StatefulWidget {
const TestScreen({
Key key,
@required this.index,
}) : super(key: key);
final int index;
@override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> with SingleTopStateMixin<TestScreen, int> {
int _index;
@override
void initState() {
super.initState();
_index = widget.index;
}
@override
void onNewIntent(int intent) {
setState(() => _index = intent);
}
@override
Widget build(BuildContext context) {
final next = _index + 1;
return Material(
child: Container(
color: Colors.primaries[_index % Colors.primaries.length],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Page ${_index}',
style: TextStyle(fontSize: 32.0),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(Activity.isSingleTop(context) ? 'Launched with Single-Top' : ''),
),
RaisedButton(
onPressed: () {
Activity.startActivity<int>(
context,
builder: (BuildContext context) => TestScreen(index: next),
intent: next,
);
},
child: Text('Next'),
),
RaisedButton(
onPressed: () {
Activity.startActivity<int>(
context,
builder: (BuildContext context) => TestScreen(index: next),
intent: next,
singleTop: true,
);
},
child: Text('Single Top'),
),
RaisedButton(
onPressed: () {
Activity.startActivity<int>(
context,
builder: (BuildContext context) => TestScreen(index: next),
intent: next,
singleTop: true,
clearTop: true,
);
},
child: Text('Single Top with Clear'),
),
],
),
),
);
}
}
/// Activity style route with single-top support
class Activity<I> extends MaterialPageRoute {
static Activity _last;
static Activity<I> of<I>(BuildContext context) {
final activity = ModalRoute.of(context);
return (activity is Activity) ? activity : null;
}
static bool isSingleTop<I>(BuildContext context) {
final activity = of<I>(context);
return (activity != null) ? activity.singleTop : false;
}
static void startActivity<I>(
BuildContext context, {
@required WidgetBuilder builder,
@required I intent,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
bool singleTop = false,
bool clearTop = false,
}) {
final activity = Activity.of<I>(context);
if (singleTop) {
if (activity != null && activity.singleTop) {
activity.onNewIntent(intent);
return;
} else if (_last != null && clearTop) {
Navigator.of(context).popUntil((route) => route == _last);
_last.onNewIntent(intent);
return;
}
}
Navigator.of(context).push(Activity<I>(
builder: builder,
intent: intent,
settings: settings,
maintainState: maintainState,
fullscreenDialog: fullscreenDialog,
singleTop: singleTop,
));
}
Activity({
@required WidgetBuilder builder,
@required I intent,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
this.singleTop = false,
}) : _intent = ValueNotifier<I>(intent),
super(
builder: builder,
settings: settings,
maintainState: maintainState,
fullscreenDialog: fullscreenDialog,
);
final ValueNotifier<I> _intent;
final bool singleTop;
I get intent => _intent.value;
void onNewIntent(I intent) => _intent.value = intent;
@override
TickerFuture didPush() {
if (singleTop) _last = this;
return super.didPush();
}
@override
bool didPop(result) {
if (_last == this) _last = null;
return super.didPop(result);
}
}
@optionalTypeArgs
mixin SingleTopStateMixin<T extends StatefulWidget, I> on State<T> {
ValueNotifier<I> _intent;
I get intent => _intent.value;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final activity = Activity.of(context);
if (activity != null) {
if (_intent != activity._intent) {
_intent?.removeListener(_onIntentChanged);
_intent = activity._intent;
_intent.addListener(_onIntentChanged);
}
}
}
void _onIntentChanged() => onNewIntent(_intent.value);
void onNewIntent(covariant I intent);
@override
void dispose() {
_intent?.removeListener(_onIntentChanged);
_intent = null;
super.dispose();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment