How I can change theme with a switch in flutter using bloc? - flutter

I am new in Flutter and bloc. I am making a app with bloc state management that can change the theme as the system theme changed. Now it work fine but I need switch that can override the theme. How can I implement that? I am making this app by watching a youtube tutorial.
Is that anyway to create that switch that can change the theme.
Theme Cubit
class ThemeCubit extends Cubit<ThemeState> {
ThemeCubit() : super(ThemeState(themeMode: ThemeMode.light)) {
updateAppTheme();
}
void updateAppTheme() {
final Brightness currentBrightness = AppTheme.currentSystemBrightness;
currentBrightness == Brightness.light
? _setTheme(ThemeMode.light)
: _setTheme(ThemeMode.dark);
}
void _setTheme(ThemeMode themeMode) {
AppTheme.setStatusBarAndNavigationBarColor(themeMode);
emit(ThemeState(themeMode: themeMode));
}
}
Theme_state
class ThemeState {
final ThemeMode themeMode;
ThemeState({#required this.themeMode});
}
Here is the code of main.dart
void main() {
Bloc.observer = AppBlocObserver();
runApp(DevicePreview(
builder: (context) => App(),
enabled: false,
plugins: [
const ScreenshotPlugin(),
],
));
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ThemeCubit>(
create: (context) => ThemeCubit(),
),
],
child: MchatsApp(),
);
}
}
class MchatsApp extends StatefulWidget {
const MchatsApp({
Key key,
}) : super(key: key);
#override
_MchatsAppState createState() => _MchatsAppState();
}
class _MchatsAppState extends State<MchatsApp> with WidgetsBindingObserver {
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangePlatformBrightness() {
context.read<ThemeCubit>().updateAppTheme();
super.didChangePlatformBrightness();
}
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return OrientationBuilder(
builder: (context, orientation) {
SizerUtil().init(constraints, orientation);
return MaterialApp(
locale: DevicePreview.locale(context),
builder: DevicePreview.appBuilder,
title: Strings.appTitle,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: context.select(
(ThemeCubit themeCubit) => themeCubit.state.themeMode),
debugShowCheckedModeBanner: false,
initialRoute: AppRouter.root,
onGenerateRoute: AppRouter.onGenerateRoute,
);
},
);
},
);
}
}

Yes, You can
In Switch Widget's onChanged function call updateAppTheme() function in your cubit
context.read<ThemeCubit>().updateAppTheme();
Builder(
builder:(context){
bool isDark= context.select(
(ThemeCubit themeCubit) => themeCubit.state.themeMode)==ThemeMode.dark?true:false;
return Switch(value: isDark, onChanged: (value) {
context.read<ThemeCubit>().updateAppTheme();}
);
})

Related

How to get flutter bloc work with routers?

Im following the documentation integrating AppRouter to my app
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() => runApp(App());
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
final _router = AppRouter();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
onGenerateRoute: _router.onGenerateRoute,
);
}
#override
void dispose() {
_router.dispose();
super.dispose();
}
}
Now in my router I need to use my bloc from my app instead of the counterbloc. This is the documentations code:
class AppRouter {
final _counterBloc = CounterBloc();
Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: _counterBloc,
child: HomePage(),
),
);
case '/counter':
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: _counterBloc,
child: CounterPage(),
),
);
default:
return null;
}
}
void dispose() {
_counterBloc.close();
}
}
In my app i have a LiveeventsBloc
...
BlocProvider<LiveeventsBloc>.value(
value: LiveeventsBloc(
RepositoryProvider.of<EventRepository>(_),
RepositoryProvider.of<SocketRepository>(_),
),
...
and I can't dispose it here since it needs two arguments.
this is the beginning of my bloc:
class LiveeventsBloc extends Bloc<LiveeventsEvent, LiveeventsState> {
final EventRepository _eventRepository;
final SocketRepository _socketRepository;
late final StreamSubscription eventStatusSubscription;
LiveeventsBloc(this._eventRepository, this._socketRepository)
: super(LiveeventsInitial()) {
...
How to dispose it in the approuter file? I dont have the context, and if i put a _ I also have an error

Why onPopPage is not being trigger in flutter Navigator 2.0?

When the back button is pressed instead of updating the state and screen the app is exited. I copied this logic from Flutter Apperenties. The same logic is working fine in another project.
I can't find the mistake. I tried to print inside _handlePopPage method but the app exits and it won't print. I think onPopPage is not being triggered.
here is my code,
class AppRouter extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
#override
final GlobalKey<NavigatorState> navigatorKey;
final AppState appStateManager;
final ProvinceProvider provinceProvider;
AppRouter({
Key? key,
required this.appStateManager,
required this.provinceProvider,
}) : navigatorKey = GlobalKey<NavigatorState>() {
appStateManager.addListener(notifyListeners);
provinceProvider.addListener(notifyListeners);
}
#override
void dispose() {
appStateManager.removeListener(notifyListeners);
provinceProvider.removeListener(notifyListeners);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (!appStateManager.isInitialized) SplashScreen.page(),
if (appStateManager.isInitialized &&
appStateManager.currentPage == CurrentPagePointer.dashboardScreen)
DashboardScreen.page(),
if (appStateManager.currentPage ==
CurrentPagePointer.openeingInformationScreen)
OpeningInformationScreen.page(),
if (appStateManager.currentPage == CurrentPagePointer.rQ1aScreen)
RQ1aScreen.page(),
if (appStateManager.currentPage == CurrentPagePointer.rQ1bScreen)
RQ1bScreen.page(),
if (appStateManager.currentPage == CurrentPagePointer.rQ1cScreen)
RQ1cScreen.page(),
if (appStateManager.currentPage == CurrentPagePointer.rQ1dScreen)
RQ1dScreen.page(),
if (appStateManager.currentPage == CurrentPagePointer.rQ1eScreen)
RQ1eScreen.page(),
if (appStateManager.currentPage == CurrentPagePointer.thankYouScreen)
ThankYouScreen.page(),
],
onPopPage: _handlePopPage,
);
}
bool _handlePopPage(Route<dynamic> route, result) {
if (!route.didPop(result)) {
return false;
}
if (route.settings.name == MaxMediaPages.openeingInformationScreen) {
print(route.settings.name);
appStateManager.setCurrentPage(CurrentPagePointer.dashboardScreen);
provinceProvider.resetData();
}
if (route.settings.name == MaxMediaPages.thankYouScreen) {
print(route.settings.name);
appStateManager.setCurrentPage(CurrentPagePointer.dashboardScreen);
}
return true;
}
#override
Future<void> setNewRoutePath(configuration) async {}
}
The first page must be only "SplashScreen.page(),": without previous condition
You probably solved this by now, but for others who encounter the same problem, please check your main.dart or the class where you call MaterialApp (and Router). It has to have a parameter: backButtonDispatcher: RootBackButtonDispatcher(),
In my case it looks like this:
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => _rallyManager,
),
ChangeNotifierProvider(
create: (context) => _positionManager,
),
ChangeNotifierProvider(
create: (context) => _appStateManager,
)
],
child: MaterialApp(
title: 'MyApp',
home: Router(
routerDelegate: _appRouter,
backButtonDispatcher: RootBackButtonDispatcher(),
),
),
);
}

Flutter. Could not find the correct Provider<ThemeChanger> above this Home Widget

I wanted to add theme with provider to my code. I adapted it from this source. https://github.com/lohanidamodar/flutter_theme_provider/blob/master/lib/main.dart .
Even it is same code, I got this error:
"The following ProviderNotFoundException was thrown building Home(dirty, state: _HomeState#c900c):
Error: Could not find the correct Provider above this Home Widget"
This happens because you used a BuildContext that does not include the provider
of your choice.
void main() async {
setPathUrlStrategy();
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MaterialAppWithTheme());
}
class MaterialAppWithTheme extends StatefulWidget {
#override
_MaterialAppWithThemeState createState() => _MaterialAppWithThemeState();
}
class _MaterialAppWithThemeState extends State<MaterialAppWithTheme> {
#override
void initState() {
super.initState();
AppRouter appRouter = AppRouter(
routes: AppRoutes.routes,
notFoundHandler: AppRoutes.routeNotFoundHandler,
);
appRouter.setupRoutes();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ThemeNotifier(),
child: Consumer<ThemeNotifier>(
builder: (context, ThemeNotifier notifier, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: notifier.darkTheme ? dark : light,
onGenerateRoute: AppRouter.router.generator,
);
},
),
);
}
}
Change this:
create: (_) => ThemeNotifier(),
To this:
create: (context) => ThemeNotifier(),

How to use ChangeNotifier with Navigator

In my app, I have a model that store the user logged in my app.
class AuthenticationModel extends ChangeNotifier {
User _user;
User get user => _user;
void authenticate(LoginData loginData) async {
// _user = // get user from http call
notifyListeners();
}
void restoreUser() async {
//_user = // get user from shared prefs
notifyListeners();
}
}
The model is registered at the top of the widget tree :
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthenticationModel(),
child: MaterialApp(
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
),
);
}
}
Somewhere down the widget tree, I have a button that calls the Model :
child: Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
return MyCustomButton(
text: 'Connect',
onPressed: () {
authModel.authenticate(...)
},
);
},
),
Now, I would like, somewhere, listen to the changes on the AuthenticationModel to trigger a Navigator.pushReplacmentNamed('/home') when the user is not null in the model.
I tried to do it in the builder of Prehome :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthenticationModel>(
builder: (context, authModel, child) {
if (authModel.user != null) {
Navigator.of(context).pushReplacementNamed("/home")
}
return Container(
child: // Prehome UI
);
},
);
}
}
but I have a error when doing it like this :
════════ (2) Exception caught by widgets library ═══════════════════════════════════════════════════
setState() or markNeedsBuild() called during build.
The relevant error-causing widget was:
Consumer<AuthenticationModel> file:///Users/pierre.degand/Projects/cdc/course_du_coeur/lib/Prehome.dart:13:12
═══════════════════════════════════════════════════════════════════════════════
How can I setup such a listener ? Is it a good practice to trigger navigation on model changes like this ?
Thanks
EDIT: I found a way to make this work. Instead of using Consumer inside the PrehomeScreen builder, I used the following code :
class PrehomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
Provider.of<AuthenticationModel>(context).addListener(() {
Navigator.of(context).pushReplacementNamed("/home");
});
return Container(
child: // UI
);
}
}
It works fine, the navigation is executed when the model changes. But there is an error message in the console (printed 3 times) :
════════ (4) Exception caught by foundation library ════════════════════════════════════════════════
Looking up a deactivated widget's ancestor is unsafe.
════════════════════════════════════════════════════════════════════════════════════════════════════
The app does not crash so, for now, I'm ok with this.
I still want to know if this is a good approach or not.
I prefer to use Stream or rxdart PublishSubject BehaviourSubject for listening to any activity or to manage global app data.
I implement it using bloc pattern. Basically bloc pattern is just like redux for react means creating a central dataset that contains all app data and you don't have to do prop drilling.
You can create Stream like this.
import 'package:rxdart/rxdart.dart';
class AbcBloc {
BehaviorSubject<bool> _connectivity;
AbcBloc() {
_connectivity = BehaviorSubject<bool>();
}
// stream
Stream<bool> get connectivity => _connectivity.stream;
// sink
Function(bool) get updateConnectivity => _connectivity.sink.add;
dispose(){
_connectivity.close();
}
}
void createAbcBloc() {
if (abcBloc != null) {
abcBloc.dispose();
}
abcBloc = AbcBloc();
}
AbcBloc abcBloc = AbcBloc();
now you can access that abcBloc variable from anywhere and listen to connectivity variable like this
import './abcBloc.dart';
void listenConnectivity(){
abcBloc.connectivity.listen((bool connectivety){
here you can perform your operations
});
}
and you can update connectivity from abcBloc.updateConnectivity(false);
every time you perform any changes that listener will get called.
remember you have to call listenConnectivity() one time to get it activated;
void main() {
Provider.debugCheckInvalidValueType = null;
return runApp(
Provider(
create: (_) => AuthenticationModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalKey<NavigatorState>();
Provider.of<AuthenticationModel>(context).addListener(() {
final authModel = Provider.of<AuthenticationModel>(context);
if (authModel.user != null) {
navigatorKey.currentState.pushReplacementNamed("/home");
}
});
return MaterialApp(
navigatorKey: navigatorKey,
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => PrehomeScreen(),
'/home': (context) => HomeScreen()
},
);
}
}
I don't think ChangeNotifier is needed.
void main() async {
final isLoggedIn = await Future.value(true); // get value from shared prefs or your model
runApp(MyApp(isLoggedIn));
}
class MyApp extends StatelessWidget {
MyApp(this.isLoggedIn);
final bool isLoggedIn;
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: isLoggedIn ? '/home' : '/',
routes: {
'/': (context) => HomeScreen(),
'/login': (context) => LoginScreen()
},
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Logout'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/login"),
);
}
}
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Login'),
onPressed: () => Navigator.of(context).pushReplacementNamed("/"),
);
}
}

How to return different MaterialApp widgets from FutureBuilder

i need to fetch data from localstorge and depending upoun that data value isLogin is true or false if isLogin value is true then return different MaterialApp and if it's false then different MaterialApp.
Widget build(BuildContext context) {
return FutureBuilder(
future: storage.ready,
builder: (BuildContext context, snapshots) {
if (snapshots.hasData) {
var isLogin = storage.getItem('isLogin');
if (snapshots.data == true) {
return MaterialApp(
initialRoute: '/sample',
onGenerateRoute: RouteGenerator.generateRoute,
);
} else {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
);
}
}
},
);
}
So here is how I did that I'm not sure if it's the best solution or not but it works.
main() async {
String isToNavigate = await MainAppService().getPrefValue('isToRemember');
String typeOfUser = await MainAppService().getPrefValue('typeOfUser');
if (isToNavigate != null) {
if (typeOfUser == 'admin') {
WidgetsFlutterBinding.ensureInitialized();
runApp(AdminMyApp());
}
if (typeOfUser == 'client') {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyAppClient());
}
if (typeOfUser == 'professional') {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyProfessionalsApp());
}
} else {
print('null is found /');
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
}
Found a solution that worked for me, maybe it helps someone in the future.
Before it was throwing me errors that weren't really making any sense:
`Null check operator used on a null value. The relevant error-causing widget was MaterialApp.
However, my solution was converting the app to a StatefulWidget, adding the future to the initState method (like you usually do it on screens as well) and then adding keys for each app.
class DEUSApp extends StatefulWidget {
const DEUSApp({Key key}) : super(key: key);
#override
_DEUSAppState createState() => _DEUSAppState();
}
class _DEUSAppState extends State<DEUSApp> {
Future<bool> initializeData;
final GlobalKey _appKey = GlobalKey();
final GlobalKey _loadingKey = GlobalKey();
#override
void initState() {
super.initState();
initializeData = context.read<SplashCubit>().initializeData();
}
#override
Widget build(BuildContext ctx) {
return BlocBuilder<SplashCubit, SplashState>(builder: (context, state) {
// at the beginning, show a splash screen, when the data hasn't been loaded yet.
return FutureBuilder(
future: initializeData,
builder: (context, snapshot) {
if (!snapshot.hasData || !(state is SplashSuccess))
return MaterialApp(key: _loadingKey, theme: MyStyles.theme, home: SplashScreen());
return MaterialApp(
key: _appKey,
title: 'Deus Finance',
debugShowCheckedModeBanner: false,
theme: MyStyles.theme,
routes: generateRoutes(context),
initialRoute: kInitialRoute,
);
},
);
});
}
}