Related
I was trying to display a progress indicator on my app on flutter while it's loading. If I don't use it, the app works properly and the data load, but when I add the progress indicator, after it disappears, the app shows only the containers' borders or colors, without the data in them. How can I solve?
Here's the code of the main page:
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
bool isLoaded = false;
#override
void initState() {
super.initState();
DataSync data = DataSync();
data.getInitialData();
globals.isLoaded.addListener(() {
setState(() {
isLoaded = true;
});
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MainPage',
home: Scaffold(resizeToAvoidBottomInset: false, body: inizializeApp() ),
debugShowCheckedModeBanner: false,
);
}
Container inizializeApp() {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomCenter,
colors: [
Color.fromARGB(255, 24, 26, 38),
Color.fromARGB(255, 1, 0, 5),
])),
//height: 500,
padding: const EdgeInsets.all(8),
child: Center(
child: isLoaded
? ListView(
physics: const NeverScrollableScrollPhysics(),
children: const [
SizedBox(height: 70, child: WeatherWidgets()),
SizedBox(height: 70, child: WeekDayWidget()),
SizedBox(height: 50, child: DateWidget()),
DataWidget(),
],
)
: const CircularProgressIndicator()),
);
}
}
and the class where I update the globals containing the data:
ValueNotifier<WeatherForecastResult> forecastResultNotifier =
ValueNotifier<WeatherForecastResult>(WeatherForecastResult.noParam());
ValueNotifier<int> selectedIndex = ValueNotifier<int>(0);
ValueNotifier<bool> isLoaded = ValueNotifier<bool>(false);
class DataSync {
Geolocation geolocation = Geolocation();
WeatherForecastResult forecastResult = WeatherForecastResult.noParam();
Weather weather = Weather();
getInitialData() async {
Position position = await geolocation.determinePosition();
forecastResult =
await weather.getForecast(position.latitude, position.longitude);
forecastResultNotifier.value = forecastResult;
isLoaded.value = true;
}
}
Tried various widget for the progress indicator, but the problem doesn't disappear
I tried to reproduce the problem - and failed! Could you check this simplified version (that should be complete in itself and run instantly) and try to reproduce the error? Or could you write a shortened version that is complete to run and reproduces the mistake? Then I would be happy to try to find out more!
import 'package:flutter/material.dart';
ValueNotifier<bool> globalIsLoaded = ValueNotifier<bool>(false);
class MyAppState extends State<MyApp> {
bool isLoaded = false;
Container initializeApp() {
return Container(
child: Center(
child: isLoaded
? const Text('is loaded')
: const CircularProgressIndicator(),
));
}
Container anotherInitializeApp() {
return Container(
child: Center(
child: ValueListenableBuilder(
builder: (BuildContext context, bool valueFromGlobalIsLoaded,
Widget? child) {
return valueFromGlobalIsLoaded
? const Text('is loaded')
: const CircularProgressIndicator();
},
valueListenable:
globalIsLoaded, // <-- ignores local isLoaded, so you would not need an addListener
),
));
}
#override
void initState() {
super.initState();
DataSync data = DataSync();
// data.getInitialData(); <-- moved it down after the addListener (but should not be decicive if this function is not super fast)
globalIsLoaded.addListener(() {
setState(() {
isLoaded = true;
});
});
data.getInitialData();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: initializeApp(), // try also: body: anotherInitializeApp(),
),
);
}
}
class DataSync {
getInitialData() async {
await Future.delayed(const Duration(seconds: 2));
globalIsLoaded.value = true;
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
MyAppState createState() => MyAppState();
}
void main() => runApp(MyApp());
I have created a screen in flutter that shows a question and displays an array of possible answers as buttons. The questions and answers are inside an AnimatedSwitcher widget, so once an answer button is clicked, the next question and answers should be displayed. Unfortunately, the AnimatedSwitcher widget only works when the button is outside its child widget. This is not the behaviour is want, since I want the answer and buttons to both be part of the animation.
Is there a way to do this or possibly a better widget to use? I'd be thankful for your help!
import 'package:flutter/material.dart';
int index = 0;
final widgets = [
QuestionWidget(
question:
Question("What is your favorite color?", ["red", "blue", "green"]),
key: const Key('1')),
QuestionWidget(
question: Question("How do you do today?", ["great", "not so well"]),
key: const Key('2')),
QuestionWidget(
question: Question("Do you like Flutter", ["yes", "no"]),
key: const Key('3')),
];
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
#override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Scaffold(
body: Center(
child: SizedBox(
width: size.width * 0.7,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) =>
SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: const Offset(0.0, 0.0))
.animate(animation),
child: FadeTransition(
opacity: animation, child: child)),
child: widgets[index]),
],
))));
}
}
class QuestionWidget extends StatefulWidget {
final Question question;
const QuestionWidget({
required this.question,
Key? key,
}) : super(key: key);
#override
State<QuestionWidget> createState() => _QuestionWidgetState();
}
class _QuestionWidgetState extends State<QuestionWidget> {
#override
Widget build(BuildContext context) {
return Column(children: [
Text(
widget.question.questionText,
style: const TextStyle(
fontSize: 25,
),
),
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.center,
spacing: 5.0,
children: [
for (var i in widget.question.answers)
ElevatedButton(
child: Text(i.toString()),
onPressed: () {
final isLastIndex = index == widgets.length - 1;
setState(() => index = isLastIndex ? 0 : index + 1);
})
],
)
]);
}
}
class Question {
String questionText;
List<String> answers;
Question(this.questionText, this.answers);
}
Storing mutable state in global variables is not a valid approach in Flutter.
One of the main rules in Flutter developement is: state goes down, events go up.
In your case, it seems that the Test widget should be responsible for defining the index of the current question, so you need to make it a part of its State. The Question widget shouldn't care about what to do when the right answer is selected, it should only know how to detect such a event and who to notify about it.
Putting it all together:
Test should store the current question index
Test should select which Question to display at the given moment
Question should notify Test when the right answer is selected
Test should change the current index in response to the event above.
In your case, notifying about the event can be nothing more than just calling a callback provided in a constructor argument.
In code:
class TestState extends State<Test> {
int _index = 0;
#override
Widget build(BuildContext context) {
...
QuestionWidget(
key: Key(index.toString()),
question: questions[index],
onCorrectAnswer: () => setState(() => index++)),
),
...
}
}
class QuestionWidget extends StatelessWidget {
final void Function() onCorrectAnswer;
#override
Widget build(BuildContext context) {
...
ElevatedButton(
onPressed: () => onCorrectAnswer(),
),
...
}
}
I highly recommend reading Flutter docs' take on state management
With the help of mfkw1's answer, I came up with this solution. I struggled a little with this which was because I did not see that the QuestionWidget was turned into a StatelessWidget.
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
#override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
int index = 0;
next() {
final isLastIndex = index == 2;
setState(() => index = isLastIndex ? 0 : index + 1);
}
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
final widgets = [
QuestionWidget(
question: Question(
"What is your favorite color?", ["red", "blue", "green"]),
key: const Key("1"),
onAnswer: () => next()),
QuestionWidget(
question: Question("How do you do today?", ["great", "not so well"]),
key: const Key("2"),
onAnswer: () => next()),
QuestionWidget(
question: Question("Do you like Flutter?", ["yes", "no"]),
key: const Key("3"),
onAnswer: () => next()),
];
var size = MediaQuery.of(context).size;
return Scaffold(
body: Center(
child: SizedBox(
width: size.width * 0.7,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) =>
SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: const Offset(0.0, 0.0))
.animate(animation),
child: FadeTransition(
opacity: animation, child: child)),
child: widgets[index]),
],
))));
}
}
class QuestionWidget extends StatelessWidget {
final Question question;
final Function() onAnswer;
const QuestionWidget({
required this.question,
required this.onAnswer,
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(children: [
Text(
question.questionText,
style: const TextStyle(
fontSize: 25,
),
),
Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.center,
spacing: 5.0,
children: [
for (var i in question.answers)
ElevatedButton(
child: Text(i.toString()),
onPressed: () {
onAnswer();
})
],
)
]);
}
}
class Question {
String questionText;
List<String> answers;
Question(this.questionText, this.answers);
}
i'm trying to implement a View with a PageView widget, that is being controlled by the ViewModel.
In my view i have buttons that trigger the following method in the ViewModel:
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
currentIndex.add(_index);
}
}
currentIndex is just a sink of my _currentIndexController, and outputCurrentIndex is a stream of the controller:
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
it looks like the value is added to the stream successfully, but i can't get it to trigger changing pages, i've set up this listener in the initState() method of the view:
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
but it is not triggered for some reason.. what am i doing wrong?
here is a full code of my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose(){
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
}
#override
Widget build(BuildContext context) {
List<Widget> pagesList = [
const SexPage(),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return MultiProvider(
providers: [
StreamProvider.value(value: _viewModel.outputProgress, initialData: 0.25),
StreamProvider.value(value: _viewModel.outputCurrentIndex, initialData: 0),
],
child: Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
),
);
}
}
class AppBarWidget extends StatefulWidget {
const AppBarWidget({
Key? key,
}) : super(key: key);
#override
State<AppBarWidget> createState() => _AppBarWidgetState();
}
class _AppBarWidgetState extends State<AppBarWidget> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: Progresso(
progress: Provider.of<double>(context),
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
),
),
),
),
Expanded(
flex: 1,
child: Provider.of<int>(context) > 1
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
),
],
);
}
}
and my ViewModel:
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
void dispose() {
_progressBarController.close();
_currentIndexController.close();
_isBackEnabled.close();
}
#override
void start() {
// TODO: implement start
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Sink get progress => _progressBarController.sink;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
}
abstract class RegisterViewModelInputs {
register();
increaseProgress();
decreaseProgress();
nextPage();
previousPage();
setIsBackEnabled(int index);
Sink get currentIndex;
Sink get isBackEnabled;
Sink get progress;
}
abstract class RegisterViewModelOutputs {
Stream<int> get outputCurrentIndex;
Stream<double> get outputProgress;
Stream<bool> get outputIsBackEnabled;
}
EDIT: I have moved the stream listener to the _AppBarWidgetState build method, and it seems to work now, but i don't fully understand why it hasn't worked before..
is it because the PageController wasn't assigned to a view yet? where is the correct place for the listener? it doesn't makes sense to me for it to be in a child widget.
Dears,
At first am so new to programming and flutter.
I bought an app code and I did the re-skin, but am facing an issue with the splash screen
the code was like this:
import 'package:cirilla/constants/assets.dart';
import 'package:cirilla/constants/constants.dart';
import 'package:flutter/material.dart';
import 'package:ui/painter/zoom_painter.dart';
import 'widgets/zoom_animation.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key, this.color, this.loading}) : super(key: key);
final Color? color;
final bool? loading;
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderStateMixin {
Size size = Size.zero;
late AnimationController _controller;
late ZoomAnimation _animation;
AnimationStatus _status = AnimationStatus.forward;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2500),
vsync: this,
)
..addStatusListener((AnimationStatus status) {
setState(() {
_status = status;
});
})
..addListener(() {
setState(() {});
});
_animation = ZoomAnimation(_controller);
}
#override
void didChangeDependencies() {
setState(() {
size = MediaQuery.of(context).size;
});
super.didChangeDependencies();
}
#override
void didUpdateWidget(covariant SplashScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (!widget.loading! && _controller.status != AnimationStatus.forward) {
_controller.forward();
}
}
#override
Widget build(BuildContext context) {
if (_status == AnimationStatus.completed) return Container();
return Stack(children: [
SizedBox(
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: ZoomPainter(color: widget.color!, zoomSize: _animation.zoomSize.value * size.width),
),
),
Padding(
padding: const EdgeInsets.only(bottom: itemPaddingExtraLarge),
child: Align(
alignment: Alignment.center,
child: Opacity(
opacity: _animation.textOpacity.value,
child: Image.asset(Assets.logo, width: 200, height: 200, fit: BoxFit.fitWidth),
),
),
)
]);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
'''
and was just fine I turned it to this
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key, this.loading}) : super(key: key);
final bool? loading;
#override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: RiveAnimation.asset('assets/splash/splash.riv',
fit: BoxFit.cover)
),
);
}
}
All I need is just to make it go to the next screen after 5 seconds, I tried many things but nothing sometimes I get a black screen after the splash screen I created with RIVE and most the time it just stuck after playing.
just to note, the following code is in home.dart
return Stack(
children: [
widget.store!.data == null ? const Empty() : buildOnBoarding(context),
SplashScreen(loading: widget.store!.loading, color: Colors.white),
],
);
}
Create method like this.Here future. delayed solve our problem to show animation 5 seconds as splash screen
setdata(BuildContext context) async {
await Future.delayed(const Duration(seconds: 5), () {
SchedulerBinding.instance!.addPostFrameCallback((_) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => WelcomeScreen()),
);
});
});
}
Splash Screen may like this
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key, this.loading}) : super(key: key);
final bool? loading;
#override
Widget build(BuildContext context) {
setdata(context);
return Scaffold(
body: Center(
child: Container(
height: 200,
width: 200,
child: RiveAnimation.network(
'https://cdn.rive.app/animations/vehicles.riv',
),
),
),
);
}
}
After splash page
welcome.dart(You must wrap the widget with Scaffold widget.other wise you get black screen
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Container(
child: Text(
"HOME PAGE",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}
SampleCode:
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(home: SimpleAnimation()));
}
setdata(BuildContext context) async {
await Future.delayed(const Duration(seconds: 5), () {
SchedulerBinding.instance!.addPostFrameCallback((_) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => WelcomeScreen()),
);
});
});
}
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key, this.loading}) : super(key: key);
final bool? loading;
#override
Widget build(BuildContext context) {
setdata(context);
return Scaffold(
body: Center(
child: Container(
height: 200,
width: 200,
child: RiveAnimation.network(
'https://cdn.rive.app/animations/vehicles.riv',
),
),
),
);
}
}
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Container(
child: Text(
"HOME PAGE",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}
I'm developing an app for Android TV, and use DPAD navigation.
I have multiple widgets inside a column. when i navigate to a widget which is outside the view, the widget/view is not moving to reflect the selected widget.
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatelessWidget(),
),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.headline4!,
child: ChangeNotifierProvider<SampleNotifier>(
create: (context) => SampleNotifier(), child: const CardHolder()),
);
}
}
class CardHolder extends StatefulWidget {
const CardHolder({Key? key}) : super(key: key);
#override
_CardHolderState createState() => _CardHolderState();
}
class _CardHolderState extends State<CardHolder> {
late FocusNode _focusNode;
late FocusAttachment _focusAttachment;
#override
void initState() {
super.initState();
_focusNode = FocusNode(debugLabel: "traversal_node");
_focusAttachment = _focusNode.attach(context, onKey: _handleKeyPress);
_focusNode.requestFocus();
}
#override
Widget build(BuildContext context) {
_focusAttachment.reparent();
return Focus(
focusNode: _focusNode,
autofocus: true,
onKey: _handleKeyPress,
child: Consumer<SampleNotifier>(
builder: (context, models, child) {
int listSize = Provider.of<SampleNotifier>(context).listSize;
return SingleChildScrollView(
child: SampleRow(cat: "Test", models: models.modelList),
);
},
),
);
}
KeyEventResult _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print("t:FocusNode: ${node.debugLabel} event: ${event.logicalKey}");
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
Provider.of<SampleNotifier>(context, listen: false).moveRight();
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
Provider.of<SampleNotifier>(context, listen: false).moveLeft();
return KeyEventResult.handled;
}
}
// debugDumpFocusTree();
return KeyEventResult.ignored;
}
}
class SampleCard extends StatefulWidget {
final int number;
final SampleModel model;
final bool focused;
const SampleCard(
{required this.number,
required this.focused,
required this.model,
Key? key})
: super(key: key);
#override
_SampleCardState createState() => _SampleCardState();
}
class _SampleCardState extends State<SampleCard> {
late Color _color;
#override
void initState() {
super.initState();
_color = Colors.red.shade900;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: widget.focused
? Container(
width: 150,
height: 300,
color: Colors.white,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
)
: Container(
width: 150,
height: 300,
color: Colors.black,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
),
);
}
}
class SampleRow extends StatelessWidget {
final String cat;
final List<SampleModel> models;
SampleRow({Key? key, required this.cat, required this.models}) : super(key: key);
#override
Widget build(BuildContext context) {
final int selectedIndex =
Provider.of<SampleNotifier>(context).selectedIndex;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16, bottom: 8),
),
models.isNotEmpty
? SizedBox(
height: 200,
child: ListView.custom(
padding: const EdgeInsets.all(8),
scrollDirection: Axis.horizontal,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SampleCard(
focused: index == selectedIndex,
model: models[index],
number: index,
),
),
childCount: models.length,
findChildIndexCallback: _findChildIndex,
),
),
)
: SizedBox(
height: 200,
child: Container(
color: Colors.teal,
),
)
],
);
}
int _findChildIndex(Key key) => models.indexWhere((model) =>
"$cat-${model.text}_${model.num}" == (key as ValueKey<String>).value);
}
class SampleNotifier extends ChangeNotifier {
final List<SampleModel> _models = [
SampleModel(0, "zero"),
SampleModel(1, "one"),
SampleModel(2, "two"),
SampleModel(3, "three"),
SampleModel(4, "four"),
SampleModel(5, "five"),
SampleModel(6, "six"),
SampleModel(7, "seven"),
SampleModel(8, "eight"),
SampleModel(9, "nine"),
SampleModel(10, "ten")
];
int _selectedIndex = 0;
List<SampleModel> get modelList => _models;
int get selectedIndex => _selectedIndex;
int get listSize => _models.length;
void moveRight() {
if (_selectedIndex < _models.length - 1) {
_selectedIndex = _selectedIndex + 1;
}
notifyListeners();
}
void moveLeft() {
if (_selectedIndex > 0) {
_selectedIndex = _selectedIndex - 1;
}
notifyListeners();
}
}
class SampleModel {
int num;
String text;
SampleModel(this.num, this.text);
}
I need a way to move/scroll the widget into view. Is there any way to do this, using the DPAD navigation on android tv
Here is the gist
You could use the scrollable_positioned_list package.
Instead of a ListView.custom which scrolls based on pixels, this widgets its based on index:
final ItemScrollController itemScrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
);
So you could maintain an index of the current scroll position and on DPAD press just :
itemScrollController.jumpTo(index: currentItem);
setState((){currentItem++;})