Skip to content

Commit

Permalink
feat: enable DarkMode in Flutter Client
Browse files Browse the repository at this point in the history
- use Theme object instead of hardcoding style

Signed-off-by: Mo <[email protected]>
  • Loading branch information
kkweon committed Jun 19, 2021
1 parent 3868874 commit e18d9ce
Show file tree
Hide file tree
Showing 11 changed files with 125 additions and 32 deletions.
44 changes: 44 additions & 0 deletions client/lib/custom_theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

/// _kHeadline1 는 Abstract/Recommendations/Repositories 등을 나타내는 헤딩1.
const _kHeadline1 = TextStyle(fontFamily: 'PermanentMarker', fontSize: 25);

/// CustomTheme 은 전체 테마를 정의합니다.
///
/// # 사용법
///
/// ```dart
/// Consumer<CustomTheme>(builder: (context, theme, _child) { ... }
/// ```
class CustomTheme extends ChangeNotifier {
static ThemeData get lightTheme {
final themeData = ThemeData();

return themeData.copyWith(
textTheme: themeData.textTheme.copyWith(headline1: _kHeadline1));
}

static ThemeData get darkTheme {
final themeData = ThemeData.dark();
final textTheme = themeData.textTheme;

return themeData.copyWith(
textTheme: textTheme.copyWith(
headline1: textTheme.headline1?.merge(_kHeadline1) ??
_kHeadline1.copyWith(color: Colors.white)));
}

bool _isDarkMode = false;

void toggleMode() {
_isDarkMode = !_isDarkMode;
notifyListeners();
}

ThemeMode get themeMode => _isDarkMode ? ThemeMode.dark : ThemeMode.light;

Icon get icon => _isDarkMode
? const Icon(Icons.dark_mode_outlined)
: const Icon(Icons.light_mode_outlined);
}
2 changes: 1 addition & 1 deletion client/lib/grpc_channel_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import 'package:grpc/grpc_web.dart';
GrpcWebClientChannel getKkweonOktetoChannel() {
return GrpcWebClientChannel.xhr(
Uri.parse("https://envoy-kkweon.cloud.okteto.net/"));
}
}
24 changes: 17 additions & 7 deletions client/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pr12er/service.dart';
import 'package:provider/provider.dart';

import 'custom_theme.dart';
import 'screens/detail_screen.dart';
import 'screens/main_screen.dart';

Expand All @@ -10,6 +11,9 @@ const appName = 'PR12er';
void main() => runApp(MultiProvider(providers: [
Provider(
create: (context) => GrpcClient(),
),
ChangeNotifierProvider(
create: (context) => CustomTheme(),
)
], child: const MainApp()));

Expand All @@ -20,13 +24,19 @@ class MainApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return MaterialApp(
title: appName,
initialRoute: MainScreen.routeName,
routes: {
MainScreen.routeName: (context) => MainScreen(),
DetailScreen.routeName: (context) => DetailScreen(),
},
return Consumer<CustomTheme>(
builder: (context, theme, _child) => MaterialApp(
title: appName,
initialRoute: MainScreen.routeName,
darkTheme: CustomTheme.darkTheme,
theme: CustomTheme.lightTheme,
themeMode: theme.themeMode,
debugShowCheckedModeBanner: false,
routes: {
MainScreen.routeName: (context) => MainScreen(),
DetailScreen.routeName: (context) => DetailScreen(),
},
),
);
}
}
9 changes: 6 additions & 3 deletions client/lib/screens/detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ class DetailScreen extends StatelessWidget {
if (detail != null) {
message.writeln('\nPaper');
message.writeln('\t- title: ${detail!.paper[0].title}');
message.writeln('\t- link: https://arxiv.org/abs/${detail!.paper[0].arxivId}');
message.writeln(
'\t- link: https://arxiv.org/abs/${detail!.paper[0].arxivId}');

message.writeln('abstract');
message.writeln('\t- ${detail!.paper[0].abstract}');
Expand All @@ -113,10 +114,12 @@ class DetailScreen extends StatelessWidget {

message.writeln('\nRecommended Papers');
for (final paper in detail!.relevantPapers) {
message.writeln('\t- ${paper.title}(${paper.authors[0]}): https://arxiv.org/abs/${paper.arxivId}');
message.writeln(
'\t- ${paper.title}(${paper.authors[0]}): https://arxiv.org/abs/${paper.arxivId}');
}
for (final paper in detail!.sameAuthorPapers) {
message.writeln('\t- ${paper.title}(${paper.authors[0]}): https://arxiv.org/abs/${paper.arxivId}');
message.writeln(
'\t- ${paper.title}(${paper.authors[0]}): https://arxiv.org/abs/${paper.arxivId}');
}
}

Expand Down
9 changes: 8 additions & 1 deletion client/lib/screens/main_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:pr12er/custom_theme.dart';
import 'package:pr12er/widgets/main/pr12video.dart';
import 'package:pr12er/widgets/main/video_search_delegate.dart';
import 'package:provider/provider.dart';
Expand All @@ -25,6 +26,12 @@ class MainScreen extends StatelessWidget {
// do something
showSearch(context: context, delegate: videoSearchDelegate);
},
),
Consumer<CustomTheme>(
builder: (context, theme, _child) => IconButton(
key: const ValueKey("icon-theme-toggle-button"),
onPressed: () => theme.toggleMode(),
icon: theme.icon),
)
],
title: const Text(appName),
Expand Down Expand Up @@ -57,4 +64,4 @@ class PRVideos extends StatelessWidget {
PR12Video(index: index, video: snapshot.data![index]));
});
}
}
}
4 changes: 2 additions & 2 deletions client/lib/widgets/detail/abstract.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class PaperAbstractWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
"Abstract",
style: TextStyle(fontFamily: 'PermanentMarker', fontSize: 25),
style: Theme.of(context).textTheme.headline1,
),
Stack(alignment: Alignment.bottomCenter, children: [
// ignore: sized_box_for_whitespace
Expand Down
8 changes: 3 additions & 5 deletions client/lib/widgets/detail/header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class HeaderWidget extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...getPresenterWidgets(),
...getPresenterWidgets(context),
const SizedBox(width: 25),
...getViewNumbersWidgets(),
const SizedBox(width: 25),
Expand Down Expand Up @@ -52,11 +52,9 @@ class HeaderWidget extends StatelessWidget {
];
}

List<Widget> getPresenterWidgets() {
List<Widget> getPresenterWidgets(BuildContext context) {
return [
Text(video.presenter,
style: const TextStyle(
color: Colors.black54, fontSize: 18, fontStyle: FontStyle.italic))
Text(video.presenter, style: Theme.of(context).textTheme.subtitle1)
];
}

Expand Down
4 changes: 2 additions & 2 deletions client/lib/widgets/detail/recommendataion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ class RecommentationWidget extends StatelessWidget {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
margin: const EdgeInsets.only(bottom: 10),
child: const Text(
child: Text(
"Recommendations",
style: TextStyle(fontFamily: 'PermanentMarker', fontSize: 25),
style: Theme.of(context).textTheme.headline1,
)),
// ignore: sized_box_for_whitespace
Container(
Expand Down
4 changes: 2 additions & 2 deletions client/lib/widgets/detail/repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class RepositoryWidget extends StatelessWidget {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
margin: const EdgeInsets.only(bottom: 10),
child: const Text(
child: Text(
"Repositories",
style: TextStyle(fontFamily: 'PermanentMarker', fontSize: 25),
style: Theme.of(context).textTheme.headline1,
)),
// ignore: sized_box_for_whitespace
Container(
Expand Down
4 changes: 2 additions & 2 deletions client/lib/widgets/main/pr12video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ class PR12Video extends StatelessWidget {
arguments: DetailScreenArguments(video),
);
},
trailing: const Icon(
trailing: Icon(
Icons.bookmark,
size: 23,
color: Colors.blue,
color: Theme.of(context).accentColor,
),
));
}
Expand Down
45 changes: 38 additions & 7 deletions client/test/screens/main_screen_test_with_mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:pr12er/custom_theme.dart';
import 'package:pr12er/protos/pkg/pr12er/messages.pb.dart';
import 'package:pr12er/screens/main_screen.dart';
import 'package:pr12er/service.dart';
import 'package:provider/provider.dart';

import 'main_screen_test_with_mocks.mocks.dart';

Widget setup(Widget widget, GrpcClient grpcClient) {
return Provider(
create: (context) => grpcClient, child: MaterialApp(home: widget));
Widget setup(
{required Widget widget, GrpcClient? grpcClient, CustomTheme? theme}) {
return MultiProvider(providers: [
Provider(create: (context) => grpcClient),
ChangeNotifierProvider(create: (context) => theme),
], child: MaterialApp(home: widget));
}

@GenerateMocks([GrpcClient])
@GenerateMocks([GrpcClient, CustomTheme])
void main() {
group("MainScreen()", () {
late List<Video> videos;
Expand All @@ -29,7 +33,8 @@ void main() {

testWidgets('MainWidget has a load view', (WidgetTester tester) async {
when(devClient.getVideos()).thenAnswer((_) => Future.value(videos));
await tester.pumpWidget(setup(MainScreen(), devClient));
await tester
.pumpWidget(setup(widget: MainScreen(), grpcClient: devClient));

final loadView = find.byType(CircularProgressIndicator);
expect(loadView, findsOneWidget);
Expand All @@ -38,10 +43,36 @@ void main() {
testWidgets("has clickable list tile", (WidgetTester tester) async {
final devClient = MockGrpcClient();
when(devClient.getVideos()).thenAnswer((_) => Future.value(videos));
await tester.pumpWidget(setup(MainScreen(), devClient));
await tester
.pumpWidget(setup(widget: MainScreen(), grpcClient: devClient));
await tester.pumpAndSettle();
final firstTile = find.byKey(const ValueKey("ListTile-0"));
expect(firstTile, findsOneWidget);
});

testWidgets("Clicking the toggle dark/light mode changes the theme",
(WidgetTester tester) async {
final devClient = MockGrpcClient();
when(devClient.getVideos()).thenAnswer((_) => Future.value(videos));

final mockTheme = MockCustomTheme();
final oldThemeMode = mockTheme.themeMode;

await tester.pumpWidget(
setup(widget: MainScreen(), grpcClient: devClient, theme: mockTheme));
await tester.pumpAndSettle();

// GIVEN the theme toggle button.
final btn = find.byKey(const ValueKey("icon-theme-toggle-button"));
expect(btn, findsOneWidget);

// WHEN the toggle button is clicked.
await tester.tap(btn);

// THEN toggleMode function has been called.
verify(mockTheme.toggleMode()).called(1);
// THEN themeMode should have changed.
expect(mockTheme.themeMode, isNot(oldThemeMode));
});
});
}
}

0 comments on commit e18d9ce

Please sign in to comment.